Implementing In-App Updates in Android

ashita asati
The Startup
Published in
6 min readJun 28, 2020

I’m sure we all must have come across the situations while working on the applications where we needed to send out an app update can say like a hotfix — maybe there’s a security issue or some bug which will break the experience for the users. In such scenarios earlier, we’ve needed to roll out a new update on the Google play store and wait for our users to receive the update. And if they don’t have automatic updates installed, we rely on the user visiting the play store to update our application or we send out a force update to the users.

Keeping your app up-to-date on your users’ devices enables them to try new features, as well as benefit from performance improvements and bug fixes.

In-app updates is a Play Core library feature that introduces a new request flow to prompt active users to update your app.

In simple words, it provides an option to the user which notifies the user that an app update is available and the user can select if they are interested in updating the app or not.

Not only that, but it also provides an option to download the update of the app in the background, and once downloaded, install the update and restart the app and the user doesn’t have to do anything.

In-app updates work only with devices running Android 5.0 (API level 21) or higher and require you to use Play Core library 1.5.0 or higher. Additionally, in-app updates support apps running on only Android mobile devices and tablets, and Chrome OS devices.

The Play Core library offers us two different ways in which we can prompt our users that an update is available — either using the Flexible or Immediate approach. Let’s take a look at each of them one by one-

Flexible: A user experience that provides background download and installation with graceful state monitoring. This UX is appropriate when it’s acceptable for the user to use the app while downloading the update. For example, you want to urge users to try a new feature that’s not critical to the core functionality of your app.

Flexible Update Flow

Immediate: A full-screen user experience that requires the user to update and restart the app to continue using the app. This UX is best for cases where an update is critical for continued use of the app. After a user accepts an immediate update, Google Play handles the update installation and app restart.

Immediate Update Flow

Now we all have an understanding of both the types of In-app updates available we will start implementing it.

The very first step is to check for the update-

Before requesting an update, you need to check if an update is available or not and also the type of update available. We do not want users to have a bad experience. To check for an update you can use the AppUpdateManager.

val appUpdateManager: AppUpdateManager = AppUpdateManagerFactory.create(baseContext)val appUpdateInfo = appUpdateManager.appUpdateInfo
// Creates instance of the manager.
val appUpdateManager = AppUpdateManagerFactory.create(context)
// 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.
appUpdateInfo?.addOnSuccessListener { appUpdateInfo ->
if
((appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) &&
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
) {
btn_update.visibility = View.VISIBLE
setUpdateAction(appUpdateManager, appUpdateInfo)
}
}

Here we have created a button to show some UX experience. Whenever there will be a flexible update available we will show the update button to the users. As we have an update available of flexible types, we will start the update.

private fun setUpdateAction(manager: AppUpdateManager?, info: Task<AppUpdateInfo>) {
// Before starting an update, register a listener for updates.
btn_update.setOnClickListener {
installStateUpdatedListener
= InstallStateUpdatedListener {
btn_update.visibility = View.GONE
// tv_status is a textview to display the update status
tv_status.visibility = View.VISIBLE
when (it.installStatus()) {
InstallStatus.FAILED, InstallStatus.UNKNOWN -> {
tv_status.text = getString(R.string.info_failed)
btn_update.visibility = View.VISIBLE
}
InstallStatus.PENDING -> {
tv_status.text = getString(R.string.info_pending)
}
InstallStatus.CANCELED -> {
tv_status.text = getString(R.string.info_canceled)
}
InstallStatus.DOWNLOADING -> {
tv_status.text = getString(R.string.info_downloading)
}
InstallStatus.DOWNLOADED -> {
tv_status.text = getString(R.string.info_downloaded)
popupSnackbarForCompleteUpdate(manager)
}
InstallStatus.INSTALLING -> {
tv_status.text = getString(R.string.info_installing)
}
InstallStatus.INSTALLED -> {
tv_status.text = getString(R.string.info_installed)
manager?.unregisterListener(installStateUpdatedListener)
}
else -> {
tv_status.text = getString(R.string.info_restart)
}
}
}
manager?.registerListener(installStateUpdatedListener)
manager?.startUpdateFlowForResult( info.result, AppUpdateType.FLEXIBLE,
this, IN_APP_UPDATE_REQUEST_CODE
)
}
}

While doing the update, you can handle the update related failure or cancellation by using the onActivityResult() as follows:

/** This is needed to handle the result of the manager.startConfirmationDialogForResult request */
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == IN_APP_UPDATE_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
toastAndLog("Update flow failed! Result code: $resultCode")
// If the update is cancelled or fails,
// you can request to start the update again.
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}

The following describes the different values you may receive from the onActivityResult() callback:

  • RESULT_OK: The user has accepted the update. For immediate updates, you might not receive this callback because the update should already be completed by Google Play by the time the control is given back to your app.
  • RESULT_CANCELED: The user has denied or canceled the update.
  • ActivityResult.RESULT_IN_APP_UPDATE_FAILED: Some other error prevented either the user from providing consent or the update to proceed.

Install a Flexible Update

If you monitor the flexible update states and you detect the InstallStatus.DOWNLOADED state, you need to restart the app to install the update.

Unlike an immediate update, Google Play does not trigger an app restart for you. That’s because, during a flexible update, the user expects to keep using the app until they decide that they want to install the update.

So, it’s recommended that you provide the user some notification or UI indication to notify that an update is ready for installation and requests user confirmation to restart the app.

Here we will implement a snackbar to request the user confirmation to restart the app.

/* Displays the snackbar notification and call to action. */
private fun popupSnackbarForCompleteUpdate(appUpdateManager: AppUpdateManager?) {
Snackbar.make(
findViewById(R.id.mainLayoutParent),
"Ready to INSTALL.", Snackbar.LENGTH_INDEFINITE
).apply {
setAction("RESTART") { appUpdateManager?.completeUpdate() }
setActionTextColor(ContextCompat.getColor(applicationContext, R.color.colorAccent))
show()
}
}

When you call appUpdateManager?.completeUpdate() in the foreground, the platform displays a full-screen UI which restarts the app in the background. After the platform installs the update, the app restarts into its main activity.

If you instead call appUpdateManager?.completeUpdate() when the app is in the background, the update is installed silently without obscuring the device UI.

When the user brings your app to the foreground, it’s recommended that you check that your app doesn’t have an update waiting to be installed. If your app has an update in the DOWNLOADED state, show the notification to request that the user install the update, as shown. Otherwise, the update data continues to occupy the user’s device storage.

// Checks that the update is not stalled during 'onResume()'.
// However, you should execute this check at all app entry points.
override fun onResume() {
super.onResume()

appUpdateManager.appUpdateInfo
.addOnSuccessListener {
if (it.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE || UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
updateManager.startUpdateFlowForResult(
it,
IMMEDIATE,
this,
IN_APP_UPDATE_REQUEST_CODE)
}
}
}
}

Handle an immediate update

If you are performing an immediate update, and the user consents to install the update, Google Play displays the update progress on top of your app’s UI across the entire duration of the update. During the update, if the user closes or terminates your app, the update should continue to download and install in the background without additional user confirmation.

appUpdateInfo?.addOnSuccessListener { appUpdateInfo ->
if
((appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) &&
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
updateManager.startUpdateFlowForResult(it, IMMEDIATE, this, IN_APP_UPDATE_REQUEST_CODE)
}
}

To check how to test In-app updates please refer: https://developer.android.com/guide/playcore/in-app-updates

Conclusion

In this blog, we’ve learned about the new approach to in-app updates that are provided by the play core library. You can also check out how to set the appUpdatePriority and clientVersionStalenessDays.

Do checkout my GitHub repo for the implementation

https://github.com/ashitaasati1608/InAppUpdate

If you have any questions about the play core library or in-app updates then please do reach out.

Happy Coding!!

--

--