I've noticed that some of the users have issues to use flexible in-app update, the JobCancellationException: Job was cancelled is thrown with incomprehensible stack trace:
at dalvik.system.VMStack.getThreadStackTrace(VMStack.java)
at java.lang.Thread.getStackTrace(Thread.java:1538)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1063)
at java.lang.Thread.dispatchUncaughtException(Thread.java:1955)
Unfortunately, I don't which part of the code is causing the issue. This is the only coroutine related code, staying in MyViewModel:
init {
viewModelScope.launch {
try {
appUpdateManager.requestUpdateFlow().collect { appUpdateResult ->
// Do something with result.
}
} catch (e: InstallException) {
// Do something with an error.
}
}
}
fun requestUpdate(fragment: Fragment) {
viewModelScope.launch {
try {
val appUpdateInfo = appUpdateManager.requestAppUpdateInfo()
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
fragment,
REQUEST_CODE
)
} catch (e: IntentSender.SendIntentException) {
}
}
}
I suspect that code inside requestUpdateFlow() is calling offer while the coroutine job is already cancelled and I can't see the exact stacktrace, because Play Core library is obfuscated?
I'm using following versions of the libraries:
"com.google.android.play:core:1.7.2"
"com.google.android.play:core-ktx:1.7.0"
JobCancellationException: Job was cancelled is thrown in almost case is job in coroutine scope is cancelled.
Example: User go to a screen a in which call api to get something. But user press back to close this screen while api not complete. Thus, when receive response, job cancelled before -> exception.
To more handle JobCancellationException you can using suspendCancellableCoroutine.
More detail : https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
Related
I was having a problem implementing the Firebase anonymous sign-in function with Kotlin coroutine.
Following is the code for that:
Repository.kt
suspend fun getUserId(){
firebaseHelper.getUserId().collect{
if (it == "Successful"){
emit(it)
} else {
emit("Task unsuccessful")
}
}
}
FirebaseHelper.kt
fun getUserId() = flow {
val firebaseLoginAsync = Firebase.auth.signInAnonymously().await()
if (firebaseLoginAsync.user != null && !firebaseLoginAsync.user?.uid.isNullOrEmpty()) {
emit("Successful")
} else {
emit("Failed")
}
}
It works fine when the android device is connected to the internet.
But when I test this code without the internet it never completes, that is, the execution never reaches the if else block of FirebaseHelper.kt.
I was unable to find any resource that would help me understand the cause of this problem and any possible solution.
One idea that I can think of on the solution side is to forcefully cancel the await() functions execution after some time but I can't find anything related to implementation.
It works fine when the android device is connected to the internet.
Since an authentication operation requires an internet connection, then that's the expected behavior.
But when I test this code without the internet it never completes.
Without the internet, there is no way you can reach Firebase servers, hence that behavior. However, according to the official documentation of await() function:
This suspending function is cancellable. If the Job of the current coroutine is canceled or completed while this suspending function is waiting, this function immediately resumes with CancellationException.
Or you can simply check if the user is connected to the internet before performing the authentication.
The way that I made it work is with help of try catch block and withTimeout() function in FirebaseHelper.kt file. Following is the code of solution:
fun getUserID() = flow {
try {
val signInTask = Firebase.auth.signInAnonymously()
kotlinx.coroutines.withTimeout(5000) {
signInTask.await()
}
if (signInTask.isSuccessful){
emit("Successful")
} else {
emit("Failed")
}
} catch (e: Exception){
emit("Can't connect to the server\nPlease check your internet connection and retry")
}
}
withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T) runs the given suspend block for timeMillis milliseconds and throws TimeoutCancellationException if the timeout was exceeded.
I try to implement deleting user in FirebaseAuth using Kotlin flow (SharedFlow).
In onDeleteAccountClicked() there is delete() method called from FirebaseAuth which may throw AuthReauthenticationRequiredException. When the exception is thrown, app redirects to another fragment to reauthenticate, then call onDeleteAccountClicked() once again, but flow emits nothing.
ViewModel
private val _deleteAccount = MutableSharedFlow<() -> Unit>()
fun onDeleteAccountClicked() {
logd("outside the viewModelScope")
viewModelScope.launch {
logd("inside the viewModelScope")
_deleteAccount.emit {
logd("emitting log")
firebaseAuth.deleteUser()
//throw AuthReauthenticationRequiredException()
}
}
}
init {
viewModelScope.launch {
_deleteAccount
.onEach {
it()
}
.catch {
if (it is AuthReauthenticationRequiredException) {
_redirectToSignInMethodsScreen.emit(Unit)
}
}
.collect()
}
}
Logs "outside the viewModelScope" and "inside the viewModelScope" shows every time when the method is called, but "emitting log" only for the first time.
Am I even trying to do it the right way?
I just tested the code, and it works for me. I called onDeleteAccountClicked() three times with delay between calling, and all three logs "emitting log" inside emit lambda were printed. Try to remove calling firebaseAuth.deleteUser() inside emit lambda and test. Calling FirebaseUser.delete function when user is already deleted throws FirebaseAuthInvalidUserException exception. Maybe that's why you didn't see logs - because FirebaseUser.delete function throws an exception.
I think the structure you use for calling just one function is a bit complicated, I can suggest to get rid of _deleteAccount flow and just wrap firebaseAuth.deleteUser() inside try-catch (you even don't need to launch a coroutine for that):
fun onDeleteAccountClicked() {
try {
firebaseAuth.deleteUser()
} catch(e: AuthReauthenticationRequiredException) {
_redirectToSignInMethodsScreen.emit(Unit)
}
}
I am trying to run a test in which I want to wait till higher order function executes. As of now I am not able to figure out any ways to do it. Following is my code.
#Test
fun `test execute routine error`() = runBlocking(coroutineDispatcher) {
val observer = mock<Observer<String>>()
baseViewModel.error.observeForever(observer)
val httpException = HttpException(Response.error<String>(402, mock(ResponseBody::class.java)))
val function = baseViewModel.executeRoutine {
throw httpException
}
verify(observer).onChanged("Something went wrong. Please try again")
}
The problem with above snippet is that it jumps to the last line i.e. verify() before throwing an http exception for executeRoutine.
Update: Execute routine definition
fun executeRoutine(requestType: RequestType = RequestType.POST_LOGIN, execute: suspend () -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
_spinner.postValue(true)
try {
execute()
} catch (ex: HttpException) {
val errorHandler = errorHandlerFactory.create(requestType)
_error.postValue(errorHandler.getErrorMessageFrom(ex))
} catch (ex: Exception) {
_error.postValue(ex.localizedMessage)
Timber.e(ex)
} finally {
_spinner.postValue(false)
}
}
}
}
The problem is that the higher order function does execute, it just doesn't do what you think it does -- its execution is launching the task, not waiting for it to complete.
You will have to solve the problem another way, by either having your test wait until the change is observed, or having the callback complete a barrier to allow the test to proceed (e.g. completableJob.complete() at the end of the call back, and completableJob.join() waiting before proceeding with the test).
It might also be desirable to rearchitect your code so you don't have to do anything special, e.g. by making executeRoutine a suspend function executing the code rather than launching the code in another scope.
Upon calling my ViewModel's saveUser(), the Firebase Firestore document is updated successfully, but the coroutine Job gets cancelled, catching a JobCancellationException, and the log "User #${user.id} saved !" is never printed. Where does this cancellation comes from and how can it complete instead ?
// ViewModel.kt
fun saveUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
Repository.saveUser(user)
Log.d("test", "User #${user.id} saved !")
}
}
// Repository.kt
suspend fun saveUser(user: User) {
val documentReference = db
.collection(USERS_COLLECTION).document(user.id)
try {
documentReference.set(user).await()
Log.d("test", "Good")
} catch (e: Exception) {
Log.e("test", "Not good") // catches a JobCancellationException
}
}
Something is cancelling the Job, and your coroutine is appropriately cooperating.
It could be that your ViewModel is being cleared, since you are launching the coroutine using viewModelScope. The ViewModel should not get destroyed unless its owner (Fragment or Activity) is being destroyed. Are you doing something like trying to call finish() on your Activity while this is happening, or performing a Fragment transaction?
Or, it could be that something else in the coroutine context is cancelling the Job due to an error. My guess would be that the call to documentReference.set(user) is causing some error, and await() is cancelling the job, maybe.
Also make sure your dependencies for Firestore, Jetpack, and the KTX extensions are up to date. This may be a bug that has been fixed.
I've searched everywhere and I haven't found anything that seems to be a solution to my problem
I have a function using coroutines:
fun onAuthenticated() {
launch (Dispatchers.IO) {
userRepo.retrieveSelf()!!.let { name ->
userRepo.addAuthenticatedAccount(name)
userRepo.setCurrentAccount(name)
}
activity?.setResult(Activity.RESULT_OK, Intent())
// this block doesn't seem to be run
withContext(Dispatchers.Main) {
Log.d(TAG, "ok looks gucci")
activity?.finish()
}
}
}
When this function is called, the code in the withContext(Dispatchers.Main) { ... } block doesn't run. I'm using it to access the activity in the main thread.
I've been getting kind of frustrated and I'm not sure if I don't understand how the dispatcher/coroutine is supposed to work or there's something I'm missing.
Let me know if you need any additional details or code!
EDIT
So Marko was right. After I moved the activity.?.setResult(Activity.RESULT_OK, Intent()) so that it was being run with the main dispatcher, I found out there was another part of the code in userRepo.setCurrentAccount(name) that was giving an issue. After updating the code as seen below, it works as expected!
override fun onAuthenticated() {
val handler = CoroutineExceptionHandler { _, e ->
Snackbar.make(
web_auth_rootview,
"Authentication unsuccessful",
Snackbar.LENGTH_INDEFINITE
).show()
}
launch(Dispatchers.Main + handler) {
userRepo.retrieveSelf()!!.let { name ->
userRepo.addAuthenticatedAccount(name)
userRepo.setCurrentAccount(name)
}
activity?.apply {
setResult(Activity.RESULT_OK, Intent())
onBackPressed()
}
}
}
Big Thanks to Marko for helping me out!
activity?.setResult(Activity.RESULT_OK, Intent())
Here you try to touch a GUI component from the IO thread. This probably throws an exception, but since it's on the IO thread, nothing catches it.
You can wrap everything in a try-catch, but your program would automatically work better if you used the proper idiom, which is to launch in the Main dispatcher and only switch to the IO context for the blocking operations:
launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
userRepo.retrieveSelf()!!.let { name ->
userRepo.addAuthenticatedAccount(name)
userRepo.setCurrentAccount(name)
}
}
activity?.setResult(Activity.RESULT_OK, Intent())
Log.d(TAG, "ok looks gucci")
activity?.finish()
}
Now, if you get an exception in the IO dispatcher, it will propagate to the top-level coroutine, which will cause an exception on the main thread, and your application will crash with it. This is a solid foundation to add your error handling logic on top of.
Of course, this is still not the way you should work with coroutines because you're missing the structured concurrency aspect.