Firebase Auth + Pixel Mobile Network + Suspend Coroutine = Bug? - android

Everything works fine, for instance, on Samsung (Android 11), Huawei (Android 10), with the exception of at least Google Pixel 2 (Android 11), Google Pixel 5 (Android 11). There is also no problem with Wi-Fi on these devices.
There is a registration screen. The user enters the data and clicks on the "sign up" button.
Everything is fine until the user performs the following actions:
Enable the mobile network -> Click on the "sign up" button -> For example, the message "email is already in use" -> Disable the mobile network -> Click on the "sign up" button -> The suspended coroutine never continues (FirebaseNetwork exception is expected)
However, it works:
Enable the mobile network -> Disable the mobile network -> Click on the "sign up" button -> For example, the message "email is already in use" (everything is fine because the suspended coroutine has woken up)
Bottom line: Firebase does not throw a FirebaseNetwork or any exception and, as a result, the user interface "freezes" (I disable the form when the request is being processed) when the user submits the form with the mobile network enabled and then submits the form with the mobile network turned off.
private suspend fun handleResult(task: Task<AuthResult>) =
suspendCoroutine<AuthResult> { continuation ->
task.addOnSuccessListener { continuation.resume(it) }
.addOnFailureListener { continuation.resumeWithException(it) }
}
I solved the problem with this answer.
The code now looks like:
private suspend fun handleResult(task: Task<AuthResult>) =
withTimeout(5000L) {
suspendCancellableCoroutine<AuthResult> { continuation ->
task.addOnSuccessListener { continuation.resume(it) }
.addOnFailureListener { continuation.resumeWithException(it); }
}
}
Do I need to use the suspendCancellableCoroutine with timeout instead of suspendCoroutine always with Firebase to avoid these bugs on another devices?

I don't know if this is the cause, but it might help. There is already a suspend function that handles Google Tasks without you having to implement suspendCancellableCoroutine yourself. Their implementation is quite a bit more thorough than yours (it's about 30 lines of code) and maybe handles some edge cases that yours doesn't. It also optimizes results when the task is already finished by the time you call it, and it handles cancellation correctly, which yours does not.
The function is Task.await(). If it's not available to you, then add this dependency in your build.gradle:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.0"

The problem is solved by combining await() (thank you, #Tenfour04) + withTimeout(). It looks like Firebase doesn't have a timeout for network authentication calls.
build.gradle to get suspending await() (replace "x.x.x" with the latest version):
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:x.x.x'
For example:
private val firebaseAuth: FirebaseAuth
suspend fun create(userInitial: UserInitial): AuthResult = withTimeout(5000L) {
firebaseAuth.createUserWithEmailAndPassword(
userInitial.email,
userInitial.password
)
.await()
}
withTimeout() throws a TimeoutCancellationException if the timeout was exceeded

Related

Delay in Google OneTap SignIn / SignUp popup display on Android

I have implemented Google Onetap SignIn in my application. Everything is working fine, the only issue that I am observing is that on certain devices the pop-up often takes 7-10 seconds to display. Especially in case of Sign-In popup.
Since I have multiple login options available in the app it might so happen that before I can show the user his last used google account to login (via OneTap popup), he gets enough time to click on any other option (eg, Facebook) & it becomes a poor experience.
Since this pop-up is displayed by play-services, I don't see how I can optimise this time taken.
As per the code, it seems the call to
contract
.getOneTapClient()
.beginSignIn(getSignInRequest(isRegistering))
is the one taking the most time. It seems the code that queries for user's on device Google Accounts.
Using below code structure. Adding for reference
contract.getOneTapClient().beginSignIn(getSignInRequest(isRegistering))
.addOnSuccessListener { result: BeginSignInResult ->
try
{
contract.startIntentSenderForResult(
result.pendingIntent.intentSender, requestCode,
null, 0, 0, 0, null)
successCallback?.onSuccess(isRegistering, "Rendering Popup")
val timeTaken = if(isRegistering) System.currentTimeMillis() - signUpTime
else System.currentTimeMillis() - signInTime
BBLogUtils.logWithTag(TAG, "Completed in ${timeTaken/1000.0}s")
}
catch (e: IntentSender.SendIntentException)
{
failureCallback?.onFailure(isRegistering, e, ERROR_INTENT_SENDER_EXCEPTION)
}
}
.addOnFailureListener { e: Exception ->
// No saved credentials found.
// OR Temporarily blocked due to too many canceled sign-in prompts.
BBLogUtils.logWithTag(TAG, "Exception | registering=$isRegistering|rCount=$rCount | Error= ${e.message}")
failureCallback?.onFailure(isRegistering, e, ERROR_NO_CREDENTIALS_FOUND)
}
SignIn request object is the standard as prescribed by the docs
private fun getSignInRequest(isRegistering: Boolean): BeginSignInRequest
{
return BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true) // So that we receive the idToken in result
.setServerClientId(contract.getGoogleAndroidClientId())
/*
* true: for Registration ie. showing all accounts
* false: for return user signIn, ie. showing only previously used accounts
**/
.setFilterByAuthorizedAccounts(!isRegistering)
.build())
.build()
}
Another related question to this feature.
On the first launch of the app on device I saw this additional pop-up
Is there someway this can be skipped ?
Answering 2nd part of my own query here.
After a lot search I still haven't bee able to find a workaround for skipping the process. Turns out that this popup is medium for play services to inform the user about the new sign-in experience.
I observed that if I installed another app using Google Onetap (eg. Reddit or PIntrest), the same pop-up appeared there as well. Also, its shown only once to a user, ie. if the pop up was shown in the Reddit app then it won't come in my app & vice-versa.
In addition, if you wan't to repro this scenario, you can clear the storage of Google Play Services. For some duration, it might show error: 16: App is not whitelisted, but after a while, you will get the How It Works pop-up again.

Synchronize coroutine from incoming messages

I'm using Android Kotlin with the SDK 30 and Coroutine 1.4.1.
I have a function that handles incoming messages to display them on my app in a form of temperature measurement. I use CoroutineScope to process the data and save it in the database. These messages are received from a socket.io connection. The problem is that the messages are not displayed in the correct order when a bulk of data is flowing in.
Now I've looked at my nodejs logs and these messages are sent in the correct order so it can't be that.
I'm using a standard Coroutine function.
See below.
fun receiveTmps(data){
CoroutineScope(IO).launch {
val usersJob = launch {
usersBg(data)
}
}
}
Now I know that with Coroutine I can add a join to wait for the job to finish before starting the next one. But because the messages do not come in at once, but flow continuously over a period of 5 to 20 seconds, it is possible that one message is completed faster than the older one. This causes incorrect order.
My question is, is there any way to handle these tasks 1 by 1 while adding multiple jobs to the list?
Any suggestion or idea is appreciated.
Thank you in advance.
UPDATED:
From what I read from the documentation you should also cancel the channel after its done. So that's going to be tricky to close the channel when messages are flowing in and since I don't have a clear number of what's flowing in I'm having a hard time defining that to the channel. I have tested several ways but most of the examples doesnt work or are outdated.
This is the most basic working example but it always has a defined repeat.
val channel = Channel<String>(UNLIMITED)
fun receiveTmps(data:String){
CoroutineScope(Dispatchers.Default).launch {
channel.send(data)
}
}
#ExperimentalCoroutinesApi
fun main() = runBlocking<Unit> {
launch {
// while(!channel.isClosedForReceive){
// val x = channel.receive()
// Log.d("deb", "Temperature.. "+ x)
// }
repeat(3) {
val x = channel.receive()
Log.d("deb", "Temperature.. "+ x)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
receiveTmps("10")
receiveTmps("30")
// Many more...
main()
}
If we need to process some events sequentially then typical solution is to create a queue of events and start a single consumer to process them. In the case of coroutines we can use Channel as a queue and launch a coroutine running in a loop that will be our consumer.
I'm not very familiar with Android, so I may miss something, but it should be something along lines:
fun receiveTmps(data:String){
channel.trySend(data).getOrThrow()
}
fun main() {
lifecycleScope.launch(Dispatchers.Default) {
for (tmp in channel) {
...
}
}
}
My assumption is that you want to stop processing events when the activity/service will be destroyed, ignoring all temps that are still waiting in the queue.

Why does Firebase pendingAuthResult return null?

I am trying to implement Sign in with Apple using Firebase Authentication. I am following the firebase/quickstart-android sample.
My sign-in fragment overrides onStart() to check for any pending results:
override fun onStart() {
super.onStart()
val pending = auth.pendingAuthResult
pending?.addOnSuccessListener { authResult ->
Timber.d("Successful login, pending")
}?.addOnFailureListener { e ->
Timber.d("Failed login, pending")
}
}
And a button that initiates the sign-in flow:
btnApple.onClick {
viewModel.appleLogin(requireActivity())
}
The viewModel calls the following method from a repository:
// Initiate sign-in flow only if there are no pending results
if (auth.pendingAuthResult != null) {
return
}
val scopes = listOf("email", "name")
val provider = OAuthProvider.newBuilder("apple.com", auth)
.setScopes(scopes)
.build()
auth.startActivityForSignInWithProvider(activity, provider)
.addOnSuccessListener { authResult ->
Timber.d("Successful login, normal")
}
.addOnFailureListener { e ->
Timber.e(e, "Failed login, normal")
}
The official manual states:
Signing in with this method puts your Activity in the background, which means that it can be reclaimed by the system during the sign in flow.
So I started testing the pending result by terminating the app in Android Studio while completing the sign-in flow in Chrome. Once I returned back to the app, the onStart() was called, but the pendingAuthResult was always null.
To make it more interesting, when I restart the app, I am logged in. Then if I log out and enter the sign-in fragment again, there is a pending result now and I receive Successful login, pending. On top of that, the pending result does not disappear. If I leave the sign-in fragment and go back, the pending result is still there and I receive yet another Successful login, pending.
I even tested the firebase/quickstart-android sample itself and it has exactly the same issue.
What could be the possible cause of this issue? I am using firebase-auth:19.2.0.
if you are using the firebase for signing with apple and if it's handled manually then you should use a string decoding mechanism, Please find the firebase guide for the same and hope it'll works. let me know if any issue!
Firebase guild signing with apple

Twilio Video Android SDK disconnects after participant lost connection

I've implemented Android client using Twilio SDK to do video calls. It works as expected but I found an edge case which I can not figure out how to fix. Here is essence of video call code:
val connectionOptions = ConnectOptions.Builder(accessToken)
.audioTracks(listOf(audioManager.getLocalAudioTrack()))
.roomName(roomId)
.build()
val roomListener = RoomListener()
Video.connect(androidContext, connectOptions, roomListener)
class RoomEventListener : Room.Listener {
override fun onParticipantDisconnected(room: Room, remoteParticipant: RemoteParticipant) {
// remove participant from the screen, unsubscribe from events
}
override fun onConnectFailure(room: Room, twilioException: TwilioException) {
exitScreenWithErrorMessage(R.string.video_consult_room_connection_error)
}
override fun onReconnected(room: Room) {
_shouldShowReconnectionActivity.value = false
}
override fun onParticipantConnected(room: Room, remoteParticipant: RemoteParticipant) {
onRemoteParticipantConnected(remoteParticipant)
}
override fun onConnected(room: Room) {
_shouldShowConnectionActivity.value = false
this#VideoCallViewModel.room = room
room.remoteParticipants.forEach { onRemoteParticipantConnected(it) }
determineMainParticipant()
onLocalParticipantConnected(room)
}
override fun onDisconnected(room: Room, twilioException: TwilioException?) {
exitVideoConsultScreen()
}
override fun onReconnecting(room: Room, twilioException: TwilioException) {
_shouldShowReconnectionActivity.value = true
}
}
Test case:
Bob joins video call using Android phone
Jane joins same video call from any device (iOS, web, Android)
When Jane loses connection (i.e. turn off internet)
Then Bob sees for 1-2 minutes reconnecting (programmatically "onReconnecting" callback triggered)
[Actual] And Bob disconnected from room (in logs I see Media connection failed or Media activity ceased with error code 53405)
[Expected] Bob stays at the room.
I'm not sure why under such conditions Android client has been disconnected (We tested it on different devices with Android 8/9).
Couple more details:
If Jane exits room using "End call" button (so room.disconnect() code from Twilio SDK has been called) then Bob stays in the room.
When Bob using iOS device (implementation of iOS and Android quite the same) then described use case passes.
We tried 5.0.1 and 5.1.0 version of com.twilio:video-android library.
I noticed Known Issue for Android Twilio Video library in Release notes and I'm not sure can it affects described use case or not:
Unpublishing and republishing a LocalAudioTrack or LocalVideoTrack might not be seen by Participants. As a result, tracks published after a Room.State.RECONNECTED event might not be subscribed to by a RemoteParticipant.
I opened issue on twilio github repo https://github.com/twilio/video-quickstart-android/issues/454 - and this is expected behaviour for twilio video sdk 5.x+. Both for Android and iOS sdks.

How do I get Nearby Messages to publish / receive from second activity?

I have got Nearby Messages apparently working fine exchanging messages happily between IOS and Android, but my Android app has multiple Activities. Once I switched to the second Activity the messages stop.
I have stripped out all the code in the Android app except for the line Nearby.getMessagesClient(this).subscribe(listener).
I then have a button to switch to a new instance of the same Activity. This works (messages are received from an IOS App that is just sending messages every 15 seconds) as a first Activity, but then fails (no messages received) once I click on the button and it starts itself.
Note that the onSuccessListner callback is triggered (the onFailureListener isn't). It thinks it is registered, but just doesn't get any messages.
I also did this with 2 copies of Activity with the same code, just to check that it wasn't because it was the same Activity class that it was failing. Still failed.
I took the Google sample App. This still uses the deprecated GooglaApiClient. It still gives the same result though.
I did spot that if the user has to take an action to enable the messages it works (as in the sample where the user has to switch messages on). I therefore tried adding a delay. A delay of 400 milliseconds means on my device that it works. 300 milliseconds and it still fails.
So I seem to have 2 choices. Add a spurious 1 second delay before enabling messages, or convert my App to use Fragments and a single Activity (I tried using the Application context and contrary to some documentation I found it tells me it must be an Activity Context). Neither are satisfactory workarounds, so am hoping that I have done something stupid.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fab.setOnClickListener { view -> // Switch to self.
val intent = Intent(this, MainActivity2::class.java)
startActivity(intent)
}
mMessageListener = object : MessageListener() {
override public fun onFound(message: Message) {
Log.d(TAG, "Found message: " + String(message.content)) //Triggered ok until Activity switch
}
}
}
override public fun onResume() {
super.onResume();
uiScope.launch {// Using coroutines as a simple means of adding a delay. It also fails with no coroutine (and no delay)
delay(300) // This fails - but a value of 400 means that messages are received
Nearby.getMessagesClient(this#MainActivity2).subscribe(mMessageListener)
.addOnSuccessListener {
Log.e(TAG, "Success") // Always triggered
}.addOnFailureListener {
Log.e(TAG, "Failed " + it) // Never triggered
}
}
}
override fun onStop() {
Nearby.getMessagesClient(this).unsubscribe(mMessageListener)
super.onStop();
}
This is the code I am currently using, but as commented above it also fails with the old API, with Java, with no coroutines, etc. The Logcat is identical in the 300 or 400 ms cases.

Categories

Resources