How do I use registerForActivityResult with StartIntentSenderForResult contract? - kotlin

I am writing a Kotlin app and using Firebase for authentication.
As onActivityResult is now depraceted, I am trying to migrate my app to use registerForActivityResult. I have a link to Google account feature, that starts with the Google sign-in flow, as shown here. My code:
private fun initGoogleSignInClient() =
activity?.let {
// Configure Google Sign In
val gso =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
// Build a GoogleSignInClient with the options specified by gso.
viewModel.googleSignInClient = GoogleSignIn.getClient(it, gso)
}
private fun showLinkWithGoogle() =
startActivityForResult(viewModel.googleSignInClient.signInIntent, RC_LINK_GOOGLE)
Where initGoogleSignInClient is called in the fragment's onCreateView, and showLinkWithGoogle is called when the user taps the button on the screen. This workes perfectly.
I looked for an example using registerForActivityResult, and the best one I found was at the bottom of this page. I added this code:
private val linkWithGoogle =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
viewModel.handleGoogleResult(it.data)
}
private fun showLinkWithGoogle() =
linkWithGoogle.launch(IntentSenderRequest.Builder(viewModel.googleSignInClient.signInIntent))
But realized that IntentSenderRequest.Builder needs an IntentSender and not an Intent. I haven't found any example of how to build an IntentSender from an Intent, nor a way to get one from my GoogleSignInClient.
Could anyone please provide a full example of using registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult())?
Thank you very much!

For this use-case, you don't need an ActivityResultContracts of type StartIntentSenderForResult but one of type StartActivityForResult. Here is an example (since you did not provide your full implementation):
Fragment
private val googleRegisterResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
result.checkResultAndExecute {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
val account = task.getResult(ApiException::class.java)
loginViewModel.onEvent(LoginRegistrationEvent.SignInWithGoogle(account))
}.onFailure { e -> toast("Error: ${e.message}") }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myGoogleSignInButton.setOnClickListener {
googleRegisterResult.launch(viewModel.googleSignInClient.signInIntent)
}
}
Then, in your viewmodel, you can handle the login as you would usually do, the only difference is, that you no longer need an RC_SIGN_IN
ViewModel Example
class YourViewModel : ViewModel() {
fun onEvent(event: LoginRegistrationEvent) {
when(event) {
is LoginRegistrationEvent.SignInWithGoogle -> {
viewModelScope.launch {
val credential = GoogleAuthProvider.getCredential(event.account.idToken)
Firebase.auth.signInWithCredential(credential).await()
}
}
}
}
}
To make my life easier, I created an extension function, that checks, if the login was successful and then executes a block of code (in this case, getting the account), while caching any exceptions. Futhermore, inside your block, you have access to an instance of ActivityResult as this:
inline fun ActivityResult.checkResultAndExecute(block: ActivityResult.() -> Unit) =
if (resultCode == Activity.RESULT_OK) runCatching(block)
else Result.failure(Exception("Something went wrong"))

Related

LiveData Observer isn't triggered for the second time

I'm expecting that the observer will be triggered when I'm hitting API by clicking one of the side menu. When I clicked one of the menu, Retrofit actually gave me the response with the correct value. The problem is, the Observer isn't getting triggered for the second time. I've trace the problem and find out that my Repository isn't returning a value even though my Retrofit already update the MutableLiveData.
RemoteDataSource.kt
override fun getDisastersByFilter(filter: String?): LiveData<ApiResponse<DisastersDTO?>> {
val result = MutableLiveData<ApiResponse<DisastersDTO?>>()
apiService.getDisastersByFilter(filter).enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if(response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
return result
}
Repository.kt
override fun getDisastersByFilter(filter: String?): LiveData<Resource<List<Disaster>>> =
remoteDataSource.getDisastersByFilter(filter).map {
when (it) {
is ApiResponse.Empty -> Resource.Error("Terjadi error")
is ApiResponse.Error -> Resource.Error(it.errorMessage)
is ApiResponse.Loading -> Resource.Loading()
is ApiResponse.Success -> Resource.Success(
DataMapper.disastersResponseToDisasterDomain(
it.data
)
)
}
}
SharedViewModel.kt
fun getDisastersByFilter(filter: String? = "gempa"): LiveData<Resource<List<Disaster>>> =
useCase.getDisastersByFilter(filter)
Here's the **MapsFragment**
private val viewModel: SharedViewModel by activityViewModels()
viewModel.getDisastersByFilter("gempa").observe(viewLifecycleOwner) {
when (it) {
is Resource.Success -> {
Log.d("MapsFragmentFilter", "${it.data}")
it.data?.let { listDisaster ->
if(listDisaster.isNotEmpty()) {
map.clear()
addGeofence(listDisaster)
listDisaster.map { disaster ->
placeMarker(disaster)
addCircle(disaster)
}
}
}
}
is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show()
is Resource.Loading -> {}
}
}
Here's the MainActivity that triggers the function to hit API
private val viewModel: SharedViewModel by viewModels()
binding.navViewMaps.setNavigationItemSelectedListener { menu ->
when (menu.itemId) {
R.id.filter_gempa -> viewModel.getDisastersByFilter("gempa")
R.id.filter_banjir -> viewModel.getDisastersByFilter("banjir")
R.id.about_us -> viewModel.getDisasters()
}
binding.drawerLayoutMain.closeDrawers()
true
}
I can't be sure from what you've posted, but your menu options call getDisastersByFilter on your SharedViewModel, and it looks like that eventually calls through to getDisastersByFilter in RemoteDataSource.
That function creates a new LiveData and returns it, and all your other functions (including the one in viewModel) just return that new LiveData. So if you want to see the result that's eventually posted to it, you need to observe that new one.
I don't know where the fragment code you posted is from, but it looks like you're just calling and observing viewModel.getDisastersByFilter once. So when that first happens, it does the data fetch and you get a result on the LiveData it returned. That LiveData won't receive any more results, from the looks of your code - it's a one-time, disposable thing that receives a result later, and then it's useless.
If I've got that right, you need to rework how you're handling your LiveDatas. The fragment needs to get the result of every viewModel.getDisastersByFilter call, so it can observe the result - it might be better if your activity passes an event to the fragment ("this item was clicked") and the fragment handles calling the VM, and it can observe the result while it's at it (pass it to a function that wires that up so you don't have to keep repeating your observer code)
The other approach would be to have the Fragment observe a currentData livedata, that's wired up to show the value of a different source livedata. Then when you call getDisastersByFilter, that source livedata is swapped for the new one. The currentData one gets any new values posted to this new source, and the fragment only has to observe that single LiveData once. All the data gets piped into it by the VM.
I don't have time to do an example, but have a look at this Transformations stuff (this is one of the developers' blogs): https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
What I believe you are doing wrong is using LiveData in the first place while using a retrofit.
You are getting a response asynchronously while your code is running synchronously. So, you need to make use of suspending functions by using suspend.
And while calling this function from ViewModel, wrap it with viewModelScope.launch{}
fun getDisastersByFilter(filter: String? = "gempa") = viewModelScope.launch {
useCase.getDisastersByFilter(filter).collect{
// do something....
// assign the values to MutableLiveData or MutableStateFlows
}
}
You should either be using RxJava or CallbackFlow.
I prefer Flows, given below is an example of how your code might look if you use callback flow.
suspend fun getDisastersByFilter(filter: String?): Flow<ApiResponse<DisastersDTO?>> =
callbackFlow {
apiService.getDisastersByFilter(filter)
.enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if (response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
trySend(ApiResponse.Success(it))
// result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
trySend(ApiResponse.Error("Bencana alam tidak ditemukan"))
// result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
trySend(ApiResponse.Error("Terjadi kesalahan!"))
// result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
trySend(ApiResponse.Error(t.localizedMessage!!))
// result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
awaitClose()
}

Why are these log statements not printing?

I'm building an object detection application (in Kotlin, for Android). The application uses CameraX to build a camera preview and Google ML to provide machine learning expertise. Just for reference; I used this CameraX documentation and this this Google ML Kit documentation.
I'm currently attempting to print Log.d("TAG", "onSuccess" + it.size) to my IDE console in order to determine if .addonSuccessListener is actually running. If it does, it should print something along the lines of onSuccess1. However, this isn't the case. Infact, it isn't even printing the Log statement from the .addOnFailureListener either, which really confuses me as I'm not even entirely sure the objectDetector code is even running. What really puzzles me is that I have relatively completed the same project in Java and have not faced this issue.
I did have someone point out that within my YourImageAnalyzer.kt class, if mediaImage is null, then I won't see anything logging. However, upon my own debugging (this is actually my very first time debugging), I was unable to find out if my first sentence of this paragraph is true or not. I suppose this issue may provide a lead into how I'll resolve this issue, and also learn how to properly debug.
Here is my YourImageAnalyzer.kt class, and I will also add the code for my MainActivity.kt class below as well.
YourImageAnalyzer.kt
private class YourImageAnalyzer : ImageAnalysis.Analyzer {
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
val image =
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val localModel = LocalModel.Builder()
.setAssetFilePath("mobilenet_v1_0.75_192_quantized_1_metadata_1.tflite")
.build()
val customObjectDetectorOptions =
CustomObjectDetectorOptions.Builder(localModel)
.setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
.enableClassification()
.setClassificationConfidenceThreshold(0.5f)
.setMaxPerObjectLabelCount(3)
.build()
val objectDetector =
ObjectDetection.getClient(customObjectDetectorOptions)
objectDetector //Here is where the issue stems, with the following listeners
.process(image)
.addOnSuccessListener {
Log.i("TAG", "onSuccess" + it.size)
for (detectedObjects in it)
{
val boundingBox = detectedObjects.boundingBox
val trackingId = detectedObjects.trackingId
for (label in detectedObjects.labels) {
val text = label.text
val index = label.index
val confidence = label.confidence
}
}
}
.addOnFailureListener { e -> Log.e("TAG", e.getLocalizedMessage()) }
.addOnCompleteListener { it -> imageProxy.close() }
}
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
override fun onCreate(savedInstanceState: Bundle?) {
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cameraProviderFuture.addListener(Runnable {
val cameraProvider = cameraProviderFuture.get()
bindPreview(cameraProvider)
}, ContextCompat.getMainExecutor(this))
}
fun bindPreview(cameraProvider: ProcessCameraProvider) {
val previewView = findViewById<PreviewView>(R.id.previewView)
var preview : Preview = Preview.Builder()
.build()
var cameraSelector : CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
var camera = cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, preview)
}
}
You are not binding your ImageAnalysis use case. Something in the line of:
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build()
and then;
imageAnalysis.setAnalyzer(executor, YourImageAnalyzer())
cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, imageAnalysis, preview)
Also a suggestion as a bonus:
You should get your LocalModel.Builder() out of analyze as this is called each time an image arrives. You do not need to execute this code piece each time as it will make your analysis slower.
So move this code:
val localModel = LocalModel.Builder()
.setAssetFilePath("mobilenet_v1_0.75_192_quantized_1_metadata_1.tflite")
.build()
to just below of the class private class YourImageAnalyzer : ImageAnalysis.Analyzer {.

CUBA Platform push messages from backend to UI

i was wondering if it is possible to send messages from the backend (for example a running task that receives information from an external system) to the UI. In my case it needs to be a specific session (no broadcast) and only on a specific screen
plan B would be polling the backend frequently but i was hoping to get something more "realtime"
I was trying to work something out like this, but i keep getting a NotSerializableException.
#Push
class StorageAccess : Screen(), MessageListener {
#Inject
private lateinit var stationWSService: StationWebSocketService
#Inject
private lateinit var notifications: Notifications
#Subscribe
private fun onInit(event: InitEvent) {
}
#Subscribe("stationPicker")
private fun onStationPickerValueChange(event: HasValue.ValueChangeEvent<StorageUnit>) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
val prevValue = event.prevValue
if (prevValue != null) {
stationWSService.remove(current.userSession.id)
}
val value = event.value ?: return
stationWSService.listen(current.userSession.id, value, this)
}
override fun messageReceived(message: String) {
val current = AppUI.getCurrent()
current.access {
notifications.create().withCaption(message).show()
}
}
#Subscribe
private fun onAfterDetach(event: AfterDetachEvent) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
stationWSService.remove(current.userSession.id)
}
}
-- The callback interface
interface MessageListener : Serializable {
fun messageReceived(message: String);
}
-- The listen method of my backend service
private val listeners: MutableMap<String, MutableMap<UUID, MessageListener>> = HashMap()
override fun listen(id: UUID, storageUnit: StorageUnit, callback: MessageListener) {
val unitStationIP: String = storageUnit.unitStationIP ?: return
if (!listeners.containsKey(unitStationIP))
listeners[unitStationIP] = HashMap()
listeners[unitStationIP]?.set(id, callback)
}
The Exception i get is NotSerializableException: com.haulmont.cuba.web.sys.WebNotifications which happens during adding the listener to the backend: stationWSService.listen(current.userSession.id, value, this)
as far as i understand this is the place where the UI sends the information to the backend - and with it the entire status of the class StorageAccess, including all its members.
is there an elegant solution to this?
regards
There is an add-on that solves exactly this problem: https://github.com/cuba-platform/global-events-addon

Can I get a random user out of the firebase realtime database as chat partner as a second option besides a selected user?

I have a problem with coding a Chat application.
One of the planned features is getting connected with a random user from the database.
Currently I have one button that loads an overview of all users, you can chose one user, and you can chat with that person (message send/receive works, also when i close the app and open it again the messages are still viewable and saved).
For that, I have a NewMessageActivity, which on click on one user leads to the ChatLogActivity and takes the user information with it (toUser and USER_KEY).
Now my plan is to basically skip the NewMessage part and get right into the ChatLogActivity, and assign a random user to that.
I was thinking to just add something like an if/else to my code (so, if toUser is assigned by NewMessageActivity load that, otherwise load random), but I can't get it to work.
My users have each individual uid's, that get created randomly when signing up.
Here's my code:
companion object {
val TAG = "ChatLog"
}
val adapter = GroupAdapter<GroupieViewHolder>()
var toUser: User? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_log)
recyclerview_chat_log.adapter = adapter
keyboardManagement()
toUser = intent.getParcelableExtra<User>(NewMessageActivity.USER_KEY)
supportActionBar?.title = toUser?.username
listenForMessages()
send_button_chat_log.setOnClickListener {
Log.d(TAG, "Attempt to send message")
performSendMessage()
}
}
Here how I usually get the users for the chat, in case that is important:
val ref = FirebaseDatabase.getInstance().getReference("/users")
ref.addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(p0: DataSnapshot) {
val adapter = GroupAdapter<GroupieViewHolder>()
p0.children.forEach{
Log.d("NewMessage", it.toString())
val user = it.getValue(User::class.java)
if (user != null) {
adapter.add(UserItem(user))
}
}
adapter.setOnItemClickListener{ item, view ->
val userItem = item as UserItem
val intent = Intent(view.context, ChatLogActivity::class.java)
//intent.putExtra(USER_KEY, userItem.user!)
intent.putExtra(USER_KEY, userItem.user)
startActivity(intent)
//Zurück zum Main Menu statt zur User Auswahl
finish()
}
recyclerView_newmessage.adapter = adapter
}
override fun onCancelled(p0: DatabaseError) {
}
})
}
}
class UserItem(val user: User): Item<GroupieViewHolder>() {
//wird aufgerufen für die einzelnen Userobjekte
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.itemView.username_textView_new_message.text = user.username
Picasso.get().load(user.profileImageURL).into(viewHolder.itemView.picture_imageView_new_message)
}
//Einzelne Zeilen gestalten
override fun getLayout(): Int {
return R.layout.user_row_new_messages
}
}
I'm still a beginner, so I'm pretty clueless what to do by now. If you have any ideas and could help me, I'd really appreciate that! :)
Thanks!

SavedState module Android Kotlin with View Model - value does not seem to save

I have followed the instructions in Google Codelab about the Saved state module.
My gradle dependency is:
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-rc03"
My View Model factory is:
class MyViewModelFactory(val repository: AppRepository, owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle): T {
return MyViewModel(repository, handle) as T
}
}
My View model:
class MyViewModel constructor(val repository: AppRepository, private val savedStateHandle: SavedStateHandle): ViewModel() {
fun getMyParameter(): LiveData<Int?> {
return savedStateHandle.getLiveData(MY_FIELD)
}
fun setMyParameter(val: Int) {
savedStateHandle.set(MY_FIELD, val)
}
}
My Fragment:
class MyFragment : androidx.fragment.app.Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
arguments?.let {
var myField = it.getInt(MY_FIELD)
activitiesViewModel.setMySavedValue(myField ?: 0)
}
}
}
When the app is being used, the live data for the saved state field updates correctly. But as soon as I put the app in background (and I set "don't keep activities" in developer options), and then re open app, the live data shows a value as if it was never set. In other words, the saved state handle seems to forget the field I am trying to save.
Any thoughts?
I had the same problem. It was solved by the fact that I stopped requesting saved values from SavedStateHandle in the onRestoreInstanceState method. This call is correctly used in the onCreate method.