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()
}
}
}
Related
I'm trying to implement In-app messaging to display a snackbar if a subscription has had it's payment declined.
Following the documentation here and adding billingClient.showInAppMessages doesn't seem to work. I subscribe using the Test card, always approves and change it to Test card, always declines and wait for the payment to be put in grace period, but the snackbar from the documentation does not show up even after restarting the application.
Expected result after payment has been declined and app was restarted:
In-app messaging works as I can send messages via firebase, but I am unsure if I'm missing something obvious here?
Implementation:
(This is called on app start)
// onCreate
billingClient = createBillingClient()
setupInAppMessaging(activity)
if (!billingClient.isReady) {
logD { "BillingClient: Start connection..." }
billingClient.startConnection(this)
}
fun createBillingClient() = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
fun setupInAppMessaging(activity: Activity) {
val inAppMessageParams = InAppMessageParams.newBuilder()
.addInAppMessageCategoryToShow(InAppMessageParams.InAppMessageCategoryId.TRANSACTIONAL)
.build()
billingClient.showInAppMessages(activity, inAppMessageParams) { inAppMessageResult ->
if (inAppMessageResult.responseCode == InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED) {
// The flow has finished and there is no action needed from developers.
logD { "SUBTEST: NO_ACTION_NEEDED"}
} else if (inAppMessageResult.responseCode == InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED) {
logD { "SUBTEST: SUBSCRIPTION_STATUS_UPDATED"}
// The subscription status changed. For example, a subscription
// has been recovered from a suspend state. Developers should
// expect the purchase token to be returned with this response
// code and use the purchase token with the Google Play
// Developer API.
}
}
}
Has it worked at all? I believe these messages are only shown once per day so if you already saw it once, you will have to wait another 24 hours to see it again if your payment method is still failing.
I use Google Play in-app in my app based the official sample project.
The Code A is to handle non-consumable products, it works well when I launch it using com.android.billingclient:billing-ktx:3.0.3 .
After I upgrade the project from Google Play Billing Library 3 to 4, I find the code purchase.sku doesn't work, so I have to replace it with purchase.skus.
The code of purchase.skus can be compiled in com.android.billingclient:billing-ktx:4.0.0, but I can't get the correct order, the test purchase is refunded after 3 minutes, it seems that Google Play doesn't acknowledge the purchase.
How can I fix the Code A when I upgrade Google Play Billing Library 3 to 4 ?
Code A
private fun processPurchases(purchasesResult: Set<Purchase>) {
val validPurchases = HashSet<Purchase>(purchasesResult.size)
purchasesResult.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (purchase.sku.equals(purchaseItem)) {
//if (purchase.skus.equals(purchaseItem)) { //sku -> skus in 4.0
if (isSignatureValid(purchase)) {
validPurchases.add(purchase)
}
}
} else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
Log.d(LOG_TAG, "Received a pending purchase of SKU: ${purchase.sku}")
// handle pending purchases, e.g. confirm with users about the pending
// purchases, prompt them to complete it, etc.
mContext.toast(R.string.msgOrderPending)
} else {
mContext.toast(R.string.msgOrderError)
}
}
acknowledgeNonConsumablePurchasesAsync(validPurchases.toList())
}
I'm not sure what is the exact reason of changing this method but I think that's probably because of the new subscriptions model in Google Play. Moreover this new method gets deprecated in the 5.0.0 version.
However, since the purchase.skus became a List of Strings, you could just check for your purchaseItem inside it. I think it depends on your purchases setup. Assuming your purchaseItem is also a String, you could either find your purchaseItem there:
if (purchase.skus.any { it == purchaseItem })
or simply take the first one to compare:
if (purchase.skus[0] == purchaseItem)
Of course, you should debug it to check what's exactly inside the skus list and then choose the best way to fix that.
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.
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()).
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