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
i decrement version build and name , so version build and version name are lower than the version on play store, i implement inapp version update of google , everything work fine, the download has complete so i add button to tell user restart application, and when he click it the install page is showen and application restart but when application start it's the same version like before , so the installation doesn't work :(
private fun showInAppUpdate() {
mAppUpdateManager = AppUpdateManagerFactory.create(activity);
mAppUpdateManager.registerListener(installStateUpdatedListener);
// Creates instance of the manager.
appUpdateManager = AppUpdateManagerFactory.create(activity)
// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
val a = appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
val b = appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
// For a flexible update, use AppUpdateType.IMMEDIATE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
// To avoid crash when startUpdateFlowForResult is called two times (by accident)
try {
appUpdateManager.startUpdateFlowForResult(
// Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo,
// Or 'AppUpdateType.FLEXIBLE' for flexible updates.
AppUpdateType.FLEXIBLE,
// The current activity making the update request.
activity,
// Include a request code to later monitor this update request.
INAPP_UPDATE_REQUEST_CODE)
}
catch (e : Exception){
e.printStackTrace()
}
}
else {
Log.e("", "checkForAppUpdateAvailability: something else");
}
}
}
val installStateUpdatedListener = object : InstallStateUpdatedListener {
override fun onStateUpdate(state: InstallState) {
if (state.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate()
} else if (state.installStatus() == InstallStatus.INSTALLED) {
mAppUpdateManager?.unregisterListener(this)
} else {
// Download or install in progress
}
}
}
/* Displays the snackbar notification and call to action. */
fun popupSnackbarForCompleteUpdate() {
val snackBar = Snackbar.make(
activity!!.findViewById(R.id.home_root),
HtmlCompat.fromHtml(this.resources.getString(R.string.inapp_downlaod_complete),
HtmlCompat.FROM_HTML_MODE_LEGACY),
Snackbar.LENGTH_INDEFINITE
)
snackBar.setTextColor(Color.WHITE)
snackBar
.apply {
setAction(activity!!.resources.getString(R.string.inapp_restart)) {
appUpdateManager.completeUpdate() }
setActionTextColor(Color.GREEN)
show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == INAPP_UPDATE_REQUEST_CODE) {
if (resultCode != AppCompatActivity.RESULT_OK) {
mAppUpdateManager?.unregisterListener(installStateUpdatedListener)
}
}
}
why the application does not update with the version on play store ?
Thank you for your help
Mostly what is happening is that you are launching your first app from your code, then trying to update that app from the store. Ultimately, those are going to be 2 different apps (since the one from your code is going to be a debug version and unsigned); therefore, you won't see the update. If you want to test this, you would need to sign the app and install it on your phone rather than just launch it from the code.
I've implemented in-app update in my Android Application with FLEXIBLE update flow. I'm checking and requesting for the update in onCreate() of MainActivity
While updating the app, always install failed with error code -100 which is mentioned at ERROR_INTERNAL_ERROR . Can anyone help me to solve this
val appUpdateManager = AppUpdateManagerFactory.create(this)
appUpdateManager.registerListener { state ->
if (state.installStatus() == InstallStatus.FAILED) {
Log.e("::MG::", "appUpdateManager:Status.FAILED")
Log.e("::MG::", "appUpdateManager:ErrorCode:"+state.installErrorCode())
}
if (state.installStatus() == InstallStatus.DOWNLOADED) {
//sBR(mainhell, resources.getString(R.string.exit))
val snackbar = Snackbar.make(
mainhell,
"Update has been downloaded. \nDo you want to install?",
Snackbar.LENGTH_INDEFINITE
)
.setAction("INSTALL") {
//If Downloaded Install Update
val i = Intent(applicationContext, MainActivity::class.java)
finish()
startActivity(i)
appUpdateManager.completeUpdate()
}
snackbar.setActionTextColor(Color.YELLOW)
snackbar.show()
}
}
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
when {
appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
-> {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
this,
FLEXIBLE_UPDATE
)
}
appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED -> {
if (appUpdateManager != null) {
appUpdateManager.completeUpdate()
}
}
}
}
Logcate
E/::MG::: appUpdateManager:InstallStatus.FAILED
E/::MG::: appUpdateManager:installErrorCode:-100
A bit late, but got this and managed to fix it : errcode -100 happens when you try to upgrade to a release apk from a debug one. It will not allow it.
This can happen if you're under a debug mode while developing, and trying to update to an APK published with release flag on the Play Store.
Solution is simply to use same targets release<->release apks update, or debug<-> debug ones (which may not be possible if your app store APK is a release one).
I recently came across a new kind of app update flow which has provided by Google Play API. I liked this seamless flow to update an Android application. I observed the below-mentioned steps in the Hotstar app.
A card popped up from the bottom showing update is available
When I clicked on "Update Hotstar" button, one dialog popped up (seems like it is provided by Google Play)
Downloading was started in the background while the app was running
After completion of the download, one SnackBar popped up showing app ready to install
App restarted after the installation
How can I achieve this? There must be a way to communicate with Google Play. I went through many blogs. But, didn't find any solution. This could be an awesome feature for a developer if the auto app update is disabled by the user.
Step 1: Add dependency (build.gradle (app)):
dependencies {
implementation 'com.google.android.play:core:1.7.3'
...
}
Step 2: Check for update availability and start if it's available
private AppUpdateManager mAppUpdateManager;
private static final int RC_APP_UPDATE = 11;
In onStart() method:
mAppUpdateManager = AppUpdateManagerFactory.create(this);
mAppUpdateManager.registerListener(installStateUpdatedListener);
mAppUpdateManager.getAppUpdateInfo().addOnSuccessListener(appUpdateInfo -> {
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE /*AppUpdateType.IMMEDIATE*/)){
try {
mAppUpdateManager.startUpdateFlowForResult(
appUpdateInfo, AppUpdateType.FLEXIBLE /*AppUpdateType.IMMEDIATE*/, MainActivity.this, RC_APP_UPDATE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
} else if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED){
//CHECK THIS if AppUpdateType.FLEXIBLE, otherwise you can skip
popupSnackbarForCompleteUpdate();
} else {
Log.e(TAG, "checkForAppUpdateAvailability: something else");
}
});
Step 3: Listen to update state
InstallStateUpdatedListener installStateUpdatedListener = new
InstallStateUpdatedListener() {
#Override
public void onStateUpdate(InstallState state) {
if (state.installStatus() == InstallStatus.DOWNLOADED){
//CHECK THIS if AppUpdateType.FLEXIBLE, otherwise you can skip
popupSnackbarForCompleteUpdate();
} else if (state.installStatus() == InstallStatus.INSTALLED){
if (mAppUpdateManager != null){
mAppUpdateManager.unregisterListener(installStateUpdatedListener);
}
} else {
Log.i(TAG, "InstallStateUpdatedListener: state: " + state.installStatus());
}
}
};
Step 4: Get a callback for update status
#Override
protected void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_APP_UPDATE) {
if (resultCode != RESULT_OK) {
Log.e(TAG, "onActivityResult: app download failed");
}
}
}
Step 5: Flexible update
private void popupSnackbarForCompleteUpdate() {
Snackbar snackbar =
Snackbar.make(
findViewById(R.id.coordinatorLayout_main),
"New app is ready!",
Snackbar.LENGTH_INDEFINITE);
snackbar.setAction("Install", view -> {
if (mAppUpdateManager != null){
mAppUpdateManager.completeUpdate();
}
});
snackbar.setActionTextColor(getResources().getColor(R.color.install_color));
snackbar.show();
}
Step 6: Don't forget to unregister listener (in onStop method)
if (mAppUpdateManager != null) {
mAppUpdateManager.unregisterListener(installStateUpdatedListener);
}
Note: Add this listener in any one activity in your app preferably in MainActivity (Home page)
For testing, you can use FakeAppUpdateManager
https://developer.android.com/reference/com/google/android/play/core/appupdate/testing/FakeAppUpdateManager.html
Constraint: In-app update works only with devices running Android 5.0 (API level 21) or higher
Official Documentation: https://developer.android.com/guide/playcore/in-app-updates
Android officially announced the in-app updates to everyone today.
https://developer.android.com/guide/playcore/in-app-updates
Update:
Handling both IMMEDIATE and FLEXIBLE updates in a single activity; Kotlin way.
import android.app.Activity
import android.content.Intent
import android.content.IntentSender
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.InstallState
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import timber.log.Timber
class BaseUpdateCheckActivity : AppCompatActivity() {
private val appUpdateManager: AppUpdateManager by lazy { AppUpdateManagerFactory.create(this) }
private val appUpdatedListener: InstallStateUpdatedListener by lazy {
object : InstallStateUpdatedListener {
override fun onStateUpdate(installState: InstallState) {
when {
installState.installStatus() == InstallStatus.DOWNLOADED -> popupSnackbarForCompleteUpdate()
installState.installStatus() == InstallStatus.INSTALLED -> appUpdateManager.unregisterListener(this)
else -> Timber.d("InstallStateUpdatedListener: state: %s", installState.installStatus())
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_ad_view)
checkForAppUpdate()
}
private fun checkForAppUpdate() {
// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
// Request the update.
try {
val installType = when {
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) -> AppUpdateType.FLEXIBLE
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) -> AppUpdateType.IMMEDIATE
else -> null
}
if (installType == AppUpdateType.FLEXIBLE) appUpdateManager.registerListener(appUpdatedListener)
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
installType!!,
this,
APP_UPDATE_REQUEST_CODE)
} catch (e: IntentSender.SendIntentException) {
e.printStackTrace()
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == APP_UPDATE_REQUEST_CODE) {
if (resultCode != Activity.RESULT_OK) {
Toast.makeText(this,
"App Update failed, please try again on the next app launch.",
Toast.LENGTH_SHORT)
.show()
}
}
}
private fun popupSnackbarForCompleteUpdate() {
val snackbar = Snackbar.make(
findViewById(R.id.drawer_layout),
"An update has just been downloaded.",
Snackbar.LENGTH_INDEFINITE)
snackbar.setAction("RESTART") { appUpdateManager.completeUpdate() }
snackbar.setActionTextColor(ContextCompat.getColor(this, R.color.accent))
snackbar.show()
}
override fun onResume() {
super.onResume()
appUpdateManager
.appUpdateInfo
.addOnSuccessListener { appUpdateInfo ->
// If the update is downloaded but not installed,
// notify the user to complete the update.
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate()
}
//Check if Immediate update is required
try {
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
// If an in-app update is already running, resume the update.
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
this,
APP_UPDATE_REQUEST_CODE)
}
} catch (e: IntentSender.SendIntentException) {
e.printStackTrace()
}
}
}
companion object {
private const val APP_UPDATE_REQUEST_CODE = 1991
}
}
Source Gist: https://gist.github.com/saikiran91/6788ad4d00edca30dad3f51aa47a4c5c
Trying to implement this, the official Google Documentation quoted in the accepted answer is syntactically incorrect. It took some research, but I finally found the correct syntax:
Instead of:
// Creates an instance of the manager.
AppUpdateManager appUpdateManager = AppUpdateManagerFactory.create(context);
// Returns an intent object that you use to check for an update.
Task<AppUpdateInfo> appUpdateInfo = appUpdateManager.getAppUpdateInfo();
// Checks that the platform will allow the specified type of update.
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
// For a flexible update, use AppUpdateType.FLEXIBLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
// Request the update.
appUpdateManager.startUpdateFlowForResult(
// Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo,
// Or 'AppUpdateType.FLEXIBLE' for flexible updates.
AppUpdateType.IMMEDIATE,
// The current activity making the update request.
this,
// Include a request code to later monitor this update request.
MY_REQUEST_CODE);
}
Do this:
private AppUpdateManager appUpdateManager;
...
// onCreate(){
// Creates instance of the manager.
appUpdateManager = AppUpdateManagerFactory.create(mainContext);
// Don't need to do this here anymore
// Returns an intent object that you use to check for an update.
//Task<AppUpdateInfo> appUpdateInfo = appUpdateManager.getAppUpdateInfo();
appUpdateManager
.getAppUpdateInfo()
.addOnSuccessListener(
appUpdateInfo -> {
// Checks that the platform will allow the specified type of update.
if ((appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE)
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE))
{
// Request the update.
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
this,
REQUEST_APP_UPDATE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
});
Then, code a similar bit of code in the onResume() override in case an install got hung up along the way:
//Checks that the update is not stalled during 'onResume()'.
//However, you should execute this check at all entry points into the app.
#Override
protected void onResume() {
super.onResume();
appUpdateManager
.getAppUpdateInfo()
.addOnSuccessListener(
appUpdateInfo -> {
if (appUpdateInfo.updateAvailability()
== UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
// If an in-app update is already running, resume the update.
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
this,
REQUEST_APP_UPDATE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
});
}
Please try this once. Official Document for reference
Step 1: In the build.gradle file add the below library (please check and update latest play code plugin version)
implementation 'com.google.android.play:core:1.6.4'
Step 2: Declare the following variables in class (Ex MainActivity.java)
private AppUpdateManager mAppUpdateManager;
private int RC_APP_UPDATE = 999;
private int inAppUpdateType;
private com.google.android.play.core.tasks.Task<AppUpdateInfo> appUpdateInfoTask;
private InstallStateUpdatedListener installStateUpdatedListener;
Step 3: In onCreate() method add the below code (initializing variables)
// Creates instance of the manager.
mAppUpdateManager = AppUpdateManagerFactory.create(this);
// Returns an intent object that you use to check for an update.
appUpdateInfoTask = mAppUpdateManager.getAppUpdateInfo();
//lambda operation used for below listener
//For flexible update
installStateUpdatedListener = installState -> {
if (installState.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate();
}
};
mAppUpdateManager.registerListener(installStateUpdatedListener);
Step 4: In onDestroy() method of activity just unregister the listener
#Override
protected void onDestroy() {
mAppUpdateManager.unregisterListener(installStateUpdatedListener);
super.onDestroy();
}
Step 5: In onResume() we need to listen to both Flexible and Immediate updates by the below code.
#Override
protected void onResume() {
try {
mAppUpdateManager.getAppUpdateInfo().addOnSuccessListener(appUpdateInfo -> {
if (appUpdateInfo.updateAvailability() ==
UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
// If an in-app update is already running, resume the update.
try {
mAppUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
inAppUpdateType,
this,
RC_APP_UPDATE);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
}
});
mAppUpdateManager.getAppUpdateInfo().addOnSuccessListener(appUpdateInfo -> {
//For flexible update
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate();
}
});
} catch (Exception e) {
e.printStackTrace();
}
super.onResume();
}
Step 6: In onActivityResult() we need to handle user click actions(only for flexible update)
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_APP_UPDATE) {
//when user clicks update button
if (resultCode == RESULT_OK) {
Toast.makeText(MainActivity.this, "App download starts...", Toast.LENGTH_LONG).show();
} else if (resultCode != RESULT_CANCELED) {
//if you want to request the update again just call checkUpdate()
Toast.makeText(MainActivity.this, "App download canceled.", Toast.LENGTH_LONG).show();
} else if (resultCode == RESULT_IN_APP_UPDATE_FAILED) {
Toast.makeText(MainActivity.this, "App download failed.", Toast.LENGTH_LONG).show();
}
}
}
Step 7: Create a method to check update available or not and start the update (Immediate update)
private void inAppUpdate() {
try {
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener(new OnSuccessListener<AppUpdateInfo>() {
#Override
public void onSuccess(AppUpdateInfo appUpdateInfo) {
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
// For a flexible update, use AppUpdateType.FLEXIBLE
&& appUpdateInfo.isUpdateTypeAllowed(inAppUpdateType)) {
// Request the update.
try {
mAppUpdateManager.startUpdateFlowForResult(
// Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo,
// Or 'AppUpdateType.FLEXIBLE' for flexible updates.
inAppUpdateType,
// The current activity making the update request.
MainActivity.this,
// Include a request code to later monitor this update request.
RC_APP_UPDATE);
} catch (IntentSender.SendIntentException ignored) {
}
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
Step 8: Finely create a snack dialog or any alert to show the user that flexible update is downloaded and ready to update(need used action to start update - only for flexible update)
private void popupSnackbarForCompleteUpdate() {
try {
Snackbar snackbar =
Snackbar.make(
findViewById(R.id.id_of_root_loyout),
"An update has just been downloaded.\nRestart to update",
Snackbar.LENGTH_INDEFINITE);
snackbar.setAction("INSTALL", view -> {
if (mAppUpdateManager != null){
mAppUpdateManager.completeUpdate();
}
});
snackbar.setActionTextColor(getResources().getColor(R.color.install_color));
snackbar.show();
} catch (Resources.NotFoundException e) {
e.printStackTrace();
}
}
Step 9: Now call the method with the in-app update type(Flexible or Immediate) wherever you want to start to check updates.
//For Immediate
inAppUpdateType = AppUpdateType.IMMEDIATE; //1
inAppUpdate();
//For Flexible
inAppUpdateType = AppUpdateType.FLEXIBLE; //0
inAppUpdate();
Points to Remember:
The flexible update will download first then it will notify the user that download completed then the user has to start the update(options given above step 8).
There is an option in google play console to test in-app sharing, just we can upload normal apk(no need signed apk)to test.
https://support.google.com/googleplay/android-developer/answer/9303479?hl=en
Need to enable in-app sharing option in your test device play store app.
How to Enable Internal App Sharing for Android?
Still, any issue in the play store, just clear cache and clear data then restart the device once and try.
My guess is that it is controlled by the app itself, rather than Google Play. I've developed apps that make an API call on startup to read the 'latest' version number and whether that version is a 'mandatory' update or not, and compares it to the running app version. If a new version is available, the user is presented with a dialog like the one you displayed (though their's is much nicer) alerting the user that an update is available. If the update is 'mandatory', then the message tells them that they must update the app before continuing. If they click Update, then they are taken to the App Store page where they initiate the download of the update as usual and the app exits. If they click Close, the app just exits. If the update is not mandatory, they are asked if they would like to update now, or continue. If they click Update, then they are taken to the App Store page where they initiate the download of the update as usual and the app exits. If they click Continue, then they are just taken into the existing version of the app.
I'm not sure how they managed the background download then kicked off the app update before exiting the app. That would be very nice, but our method above was also very easy and gives a lot of capability to the developer.
Try these libraries, where you can implement in few lines of code.
https://github.com/SanojPunchihewa/InAppUpdater
https://github.com/dnKaratzas/android-inapp-update
Google is testing an early version of an In-apps update API as described at this blog post.
It's only available for some early testing partners right now, but it should be available for all developers eventually. Keep your eye out on the Android Developers Blog and for announcements in the Play console.
So I am currently chaining API calls together using flatMap and it is working very well for my use cases. If one of my calls return a failing response code, then I pass an error single that contains a throwable with a message that says which call failed and it keeps going. Here is how I'm doing it now:
dataManager.apiCall1(dataManager.sessionId!!)
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.flatMap{apiCall1Response ->
if (apiCall1Response.isSuccessful && apiCall1Response.body() != null) {
// First api call was successful, execute api call 2.
return#flatMap dataManager.apiCall2(apiCall1Response.importantVal)
} else {
// First api call failed
return#flatMap Single.error<Throwable>(Throwable("First api call failed."))
}
}.flatMap{apiCall2Response ->
if (apiCall2Response != null && apiCall2Response.isSuccessful && apiCall2Response.body() != null) {
// Second api call was successful, execute api call 3.
return#flatMap dataManager.apiCall3(apiCall2Response.importantVal)
} else if (apiCall2Response is Throwable) {
// Api Call 1 Failed.
return#flatMap Single.error<Throwable>(apiCall2Response)
} else {
// Second api call failed
return#flatMap Single.error<Throwable>(Throwable("Second api call failed."))
}
}.subscribe({apiCall3Response ->
if (apiCall3Response is Response<*> && apiCall3Response.body() != null) {
// Success!
navigator?.successful(response)
} else if (apiCall3Response is Throwable) {
// Something failed from before.
navigator?.handleError(apiCall3Response)
} else {
// Third api call failed, handle error
navigator!!.handleError(Throwable("Api call 3 failed."))
}
}, {throwable ->
navigator!!.handleError(throwable)
})
Well, now I am realizing that I need to make a different api call if my first api call is successful and any of my other calls fail. This is a sequence of calls to log a user in, if the login call is successful, but the next call fails, we would need to call the api logout endpoint. I know it is bad practice to create another single inside of the subscribe() method, so I don't want to do that. I would rather pass the logout call through, but the problem is that there is no way of knowing which api call is being returned in the subscribe method since both the logout and apiCall3 return empty bodies. I would also like to call the logout endpoint if apiCall3 fails, but not sure if that is possible. Here is what I am trying to do:
dataManager.apiCall1(dataManager.sessionId!!)
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.flatMap{apiCall1Response ->
if (apiCall1Response.isSuccessful && apiCall1Response.body() != null) {
// First api call was successful, execute api call 2.
return#flatMap dataManager.apiCall2(apiCall1Response.importantVal)
} else {
// First api call failed
return#flatMap Single.error<Throwable>(Throwable("First api call failed."))
}
}.flatMap{apiCall2Response ->
if (apiCall2Response != null && apiCall2Response.isSuccessful && apiCall2Response.body() != null) {
// Second api call was successful, execute api call 3.
return#flatMap dataManager.apiCall3(apiCall2Response.importantVal)
} else if (apiCall2Response is Throwable) {
// Api Call 1 Failed.
return#flatMap Single.error<Throwable>(apiCall2Response)
} else {
// Second api call failed, logout
return#flatMap dataManager.logoutApiCall()
}
}.subscribe({apiCall3OrLogoutResponse ->
// I would like to be able to determine which call this response is from. That is the question.
if (apiCall3OrLogoutResponse is Response<*> && apiCall3OrLogoutResponse.body() != null) {
// Success!
navigator?.successful(response)
} else if (apiCall3OrLogoutResponse is Throwable) {
// Something failed from before.
navigator?.handleError(apiCall3OrLogoutResponse)
} else {
// Third api call or logout call failed, handle error
if (apiCall3OrLogoutResponse is ApiCall3) {
// Api Call 3 failed.
// Somehow call logout api endpoint
} else if (apiCall3OrLogoutResponse is LogoutCall {
// Logout call failed.
navigator?.handleError(Throwable("Logout failed."))
}
}
}, {throwable ->
navigator!!.handleError(throwable)
})
Is there a better way to do this? My use cases are making three sequential api calls, if the first one fails, send a throwable to the subscriber, if the first one succeeds and any fail after that, make another api call.
I figured it out by throwing custom exceptions instead of passing Single.error down and having my final call checked in flatMapCompletable rather than in the subscription. Then I called the logout endpoint in doOnError if the exception isn't a login exception.
dataManager.apiCall1(dataManager.sessionId!!)
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.flatMap{apiCall1Response ->
if (apiCall1Response.isSuccessful && apiCall1Response.body() != null) {
// First api call was successful, execute api call 2.
return#flatMap dataManager.apiCall2(apiCall1Response.importantVal)
} else {
// First api call failed
throw ApiCall1Exception("Api Call 1 failed.")
}
}.flatMap{apiCall2Response ->
if (apiCall2Response != null && apiCall2Response.isSuccessful && apiCall2Response.body() != null) {
// Second api call was successful, execute api call 3.
return#flatMap dataManager.apiCall3(apiCall2Response.importantVal)
} else {
// Second api call failed
throw Throwable("Api call 2 failed.")
}
}.flatMapCompletable{apiCall3Response ->
if (apiCall3Response.body() != null) {
// All api calls were successful!
Completable.complete()
} else {
// Third api call failed.
throw Throwable("Api call 3 failed.")
}
}.doOnError{throwable ->
if (throwable !is ApiCall1Exception) {
// Api call 1 was successful, but something else failed, call logout endpoint.
dataManager.logout()
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
}
}.subscribe({
// Success!
navigator?.success()
}, {throwable ->
// Something failed!
navigator?.handleError(throwable)
})