Crash in Android BillingClient with coroutines - android

I'm getting notified that my Billing solution is crashing in a weird way. I'm unable to reproduce it or find a fix/bypass the problem. Maybe you could help.
Fatal Exception: java.lang.IllegalStateException: Already resumed
at kotlin.coroutines.SafeContinuation.resumeWith + 45(SafeContinuation.java:45)
at com.android.billingclient.api.BillingClientKotlinKt$querySkuDetails$2$1.onSkuDetailsResponse + 2(BillingClientKotlinKt.java:2)
at com.android.billingclient.api.zzj.run + 8(zzj.java:8)
at android.os.Handler.handleCallback + 907(Handler.java:907)
at android.os.Handler.dispatchMessage + 105(Handler.java:105)
at android.os.Looper.loop + 216(Looper.java:216)
at android.app.ActivityThread.main + 7625(ActivityThread.java:7625)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run + 524(RuntimeInit.java:524)
at com.android.internal.os.ZygoteInit.main + 987(ZygoteInit.java:987)
//billing
implementation 'com.android.billingclient:billing:2.2.0'
implementation 'com.android.billingclient:billing-ktx:2.1.0'

Since last week we have this code running to prevent the exception:
suspend fun BillingClient.querySkuDetailsFixed(params: SkuDetailsParams) = suspendCancellableCoroutine<SkuDetailsResult> { continuation ->
querySkuDetailsAsync(params) { billingResult, skuDetails: List<SkuDetails>? ->
if (continuation.isActive) {
// doing some logging
continuation.resumeWith(Result.success(SkuDetailsResult(billingResult, skuDetails)))
} else {
// Already resumed, doing some logging
}
}
}
In the logs we see that we get 2 calls from the library:
The first response has always the BillingResponseCode -3 SERVICE_TIMEOUT.
The second response often has either 6 ERROR or 2 SERVICE_UNAVAILABLE.
In our case this is happening when the app is awaken in the background for a PeriodicWorkRequest.

Update Google Play Billing Library to version 3.0.2 or newer. It was a bug in a library that is fixed according to release notes:
Fixed a bug in the Kotlin extension where the coroutine fails with error "Already resumed".

I can 100% reproduce this crash, even using latest billing 5.0.0:
"com.android.billingclient:billing-ktx:5.0.0".
I'm trying to wrap callback-base API for billing into coroutines:
private suspend fun doStart() {
if (billingClient.isReady) {
return
}
suspendCoroutine { cont ->
val callback = object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
cont.resumeWithException(RuntimeException())
}
override fun onBillingSetupFinished(result: BillingResult) {
when (result.responseCode) {
BillingClient.BillingResponseCode.OK -> cont.resume(true)
else -> cont.resumeWithException(RuntimeException())
}
}
}
billingClient.startConnection(callback)
}
}
So, I start my billingClient lazily in onResume of single MainActivity and load active in-app purchases. As you can see from this method doStart(), it first checks billingClient.isReady, to avoid multiple starts.
I also have open-app ads integrated to my app, and let's say Open App Ad starts every 2nd COLD open of the app (COLD - after the app being killed, i.e. in onCreate()). We Open App Ad shows, it covers the entire screen.
So the crash happens in 100% cases when it's about time to show Open App Ad. Some conflict between started->then->resumed MainActivity and Ad proxy Activity. Stacktrace is exactly same, pointing to the line in onBillingServiceDisconnected():
cont.resumeWithException(RuntimeException()).

Related

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

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

How to handle InstallException in in-app updates?

I am trying to implement in-app updates in my android app following the official documentation.
I launched one version of my app on Play Store using internal testing track followed by another version with incremented versionCode.
When I opened the app first time it crashed with the following exception:
Fatal Exception: com.google.android.play.core.install.InstallException: Install Error(-10): The app is not owned by any user on this device. An app is "owned" if it has been acquired from Play. (https://developer.android.com/reference/com/google/android/play/core/install/model/InstallErrorCode#ERROR_APP_NOT_OWNED)
at com.google.android.play.core.appupdate.o.a(o.java:6)
at com.google.android.play.core.internal.o.a(o.java:28)
at com.google.android.play.core.internal.j.onTransact(j.java:20)
at android.os.Binder.execTransactInternal(Binder.java:1166)
at android.os.Binder.execTransact(Binder.java:1130)
But when I reopened the app, the update flow started properly. So maybe the PlayCore library wasn't able to fetch the right data first time and it threw the InstallException.
What I want is to catch all such InstallExceptions but I am not able to find where exactly to put the try-catch block. Which function of AppUpdateManager throws this InstallException? Is it the startUpdateFlow() method?
My code:
private lateinit var updateInfo: AppUpdateInfo
suspend fun checkForUpdate() {
updateInfo = appUpdateManager.requestAppUpdateInfo() // suspend function from play-core-ktx
if(updateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && updateInfo.isImmediateUpdateAllowed) {
startImmediateUpdate(activity)
}
}
fun startImmediateUpdate(activity: Activity) {
appUpdateManager.startUpdateFlow( // Here I am using startUpdateFlow and not startUpdateFlowForResult
updateInfo, activity, AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE)
).addOnSuccessListener { result ->
if (result == Activity.RESULT_CANCELED) {
activity.finish()
}
}
}

How to close/finish my Vungle rewrded video ad programatically, as if user closes the app or put in background?

I am implementing the Vungle ads in my android app,
the problem is when the user closes the app by swiping up the app after pressing the overview button (the right one from default buttons), meanwhile, Vungle rewarded ad was running,
After closing the application while the rewarded ad is running Vungle ad behavior is weird,
when the user open the app again it never shows the new ad, even ad is available (canPlayAd returns true) but ad could not be played,
or sometimes rewarded call back runs, even app is closed by the user.
I just want to destroy/close the Vungle rewarded ad, when the application's main activity's onDestroy called. Thanks
if (Vungle.canPlayAd(placementId)) {
editActivityUtils.logGeneralEvent(context, "rewardedVdoPlayed", "$cat_name: $name")
firebaseAnalytics.setUserProperty("rewardedVdoPlayed", "$cat_name")
Vungle.playAd(placementId, adConfig, object : PlayAdCallback {
override fun onAdStart(id: String) {}
override fun onAdEnd(id: String, completed: Boolean, isCTAClicked: Boolean) {
Log.e("app", "Vungle ad end")
if ((!(context as TemplatesMainActivity).isDestroyed) && completed) {
Vungle.loadAd(placementId, object : LoadAdCallback {
override fun onAdLoad(id: String?) {
}
override fun onError(id: String?, exception: VungleException?) {
}
})
Log.e("app", "Vungle ad rewarded")
editActivityUtils.logGeneralEvent(context, "gotTemplateByRewardedVdo", "$cat_name: $name")
firebaseAnalytics.setUserProperty("gotTemplateByRewardedVdo", "$cat_name")
goToEditorWithoutAD(cat_name, name)
}
}
override fun onAdEnd(id: String) {}
override fun onAdClick(id: String) {}
override fun onAdRewarded(id: String) {
Log.e("app", "Vungle ad rewarded")
}
override fun onAdLeftApplication(id: String) {
Log.e("app", "Vungle left app")
}
override fun onError(id: String, exception: VungleException) {
Log.e("app", "Vungle ${exception.localizedMessage}")
}
})
}
Sorry, your questions do not include enough information for me to give you an answer.
Please submit a ticket to Vungle tech-support#vungle.com for help.
Below are some questions for the issue, please include the information and send to Vungle. The information should help Vungle to know what exactly happened on your side.
I am implementing the Vungle ads, the problem is when the user
destroys the application while Vungle rewarded ad was running, after
that Vungle ad behavior is weird,
it never shows the new ad, even ad is available,
-- In this case, yes, it looks weird. Have you checked the ad available before calling the play function?
or sometimes rewarded call back runs, even app is closed by the user.
-- Not sure which callback did you exactly mean. Please show the code if possible.
I just want to destroy Vungle rewarded ad, when activity's onDestroy
called.
-- There should be nothing you need to do to destroy Vungle rewarded ads.
Vungle should be able to help you figure out the weird behavior.
Please provide also provide information to Vungle if you have:
Platform(s)(iOS, Android, Windows):
Device and operating system version:
Vungle APP ID(s):
APP Name(s):
Application Status, i.e Testmode /Active / Inactive :
Vungle SDK Version(s):
Which type of Vungle SDK integration? (iOS, Android, Windows, or Plugin):
If using “Mediation” service, which one:

Google InApp Update Not working - Always returns UPDATE_NOT_AVAILABLE

I am not able to see the Google In-App Updates working in my application. Here is the way I have implemented my code, but it appUpdateInfo always returns UPDATE_NOT_AVAILABLE (1).
onCreate Method
appUpdateManager = AppUpdateManagerFactory.create(this)
appUpdateManager.registerListener(this)
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener {appUpdateInfo ->
try {
if(appUpdateInfoTask.isComplete){
if (appUpdateInfo.updateAvailability() == UPDATE_AVAILABLE) {
ToastUtils.showShort("Update Available" + appUpdateInfo.isUpdateTypeAllowed(FLEXIBLE))
if(appUpdateInfo.isUpdateTypeAllowed(FLEXIBLE)){
ToastUtils.showShort("Flexi Update")
requestAppUpdate(appUpdateManager.appUpdateInfo.getResult(), FLEXIBLE)
}else if(appUpdateInfo.isUpdateTypeAllowed(IMMEDIATE)){
ToastUtils.showShort("Immediate Update")
requestAppUpdate(appUpdateManager.appUpdateInfo.getResult(), IMMEDIATE)
}
}
}
} catch (e: Exception) {
Log.e("Update Exception", e.message)
}
}
onResume Method
override fun onResume() {
super.onResume()
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnCompleteListener {
Log.e("Update Complete", "Complete")
}
appUpdateInfoTask.addOnSuccessListener {
if(appUpdateInfoTask.isComplete){
if (it.installStatus() == DOWNLOADED) {
showUpdateSnackbar()
}
}
}
}
onDestroy Method
override fun onDestroy() {
super.onDestroy()
appUpdateManager.unregisterListener(this)
}
onActivityResult Method
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_TO_FETCH_UPDATES) {
when (resultCode) {
Activity.RESULT_OK -> {
ToastUtils.showShort("Access to Update Granted")
}
Activity.RESULT_CANCELED -> {
ToastUtils.showShort("Access to Update Cancelled")
}
ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> {
ToastUtils.showShort("Access to Update Failed")
}
}
}
}
Key Points
Uploaded my app on the internal test track with Android App Bundle Format
App Update is available on the store(on internal track) when my code written above in onCreate returns UPDATE_NOT_AVAILABLE.
I have uploaded the using Google Play Developers API, and have set the inAppUpdatePriority as 5
QUERIES:
I have tried updating the app many times on the store and I can never see an update on my app via this code.WHY?
How can I see actual FLEXIBLE or IMMEDIATE Update by testing from the Internal test track? Where is the way to set that configuration?
I have recently implemented in app date successfully so I think I may be able to help you.
I may suggest you to make some changes in your code and also you must follow the correct way of testing with internal app sharing.
Inside the try block in OnCreate Method you can use this
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE){
if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
this,
REQUEST_CODE_TO_FETCH_UPDATES)
}
else if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
this,
REQUEST_CODE_TO_FETCH_UPDATES)
}
}
To test the in app update feature you must follow the following points-
You must make sure that the current app on your device is installed using an internal app sharing URL and the current app already supports in-app updates and also its version code must be lower than the updated version in Internal App Sharing.
If above all is fine, only click the internal app-sharing link for the updated version of your app. Do not install the app from the Google Play Store page you see after clicking the link.
Now when you open the app you will find the update in your app.
Or in simple way -
Step 1- Just build your app with the app update features and upload it to the Internal App Testing in Play Console
Step 2- Now go to the link obtained after uploading the app and install this version of your app from Play Store.
Step 3- Now again build your app with a higher version code than the previous one and again upload it to the Internal App Testing in Play Console
Step 4 - Now again go to the link obtained after uploading the app but now don't install this update just after going to Play Store page, press back and exit.
Step 5 - Now open your previously installed app and you will find the app update in your app
Hope it helps you.

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