In-app Updates: be prepared for the critical-bug-case

In-app Updates: be prepared for the critical-bug-case

No matter how experienced you are, no matter how careful you are, no matter how good your test coverage is, a major bug in your app will occur.

For web development that's not a big deal. It's just a server push away. But for mobile apps is a whole different story. Yes, you can push immediately to the store. But users will get the update when it's convenient for their network connection and battery (i.e. by default auto-update takes place when on wifi and charging). In fact, for apps with a sizable install base, it's almost certain that some users will fall behind and keep running older versions.

But your app is not working with that ugly major bug. And there's nothing you can do except waiting patiently for all your users to update on their pace.

Except if you build an "emergency" update mechanism that you can trigger for these situations. Thankfully, the In-app Update library handles all the difficult bits. It offers 2 kinds of in-app updates:

  • A "flexible" update flow that just nudges the user to update the app. This is a friendly reminder that there's a new update available, but the user is free to dismiss the dialog and continue without updating.
  • An "immediate" update flow that "forces" users to update. This is more suitable in the critical-bug-case I described above where the user must update the app to use it.

Triggers

The library provides a way to show the above flows. But you will decide when these flows should be shown. A good idea is to use a remote config service (such as Firebase Remote Config). You can remotely set a "minimum" version that your users should be running (i.e. this version contains the fix to the "critical" bug). Users below that minimum version will be shown the immediate update flow. For the flexible flow, you can set some days since you roll out an update that you wish to nudge the user to update.

Set up

In-app Update library is part of the Play Core library. Add these lines to your build.gradle. Check the docs for the latest version.

dependencies {
    [...]
    implementation 'com.google.android.play:core:1.8.3'
    implementation 'com.google.android.play:core-ktx:1.8.1'
}

Immediate update flow

companion object {
    const val REQUEST_CODE_IMMEDIATE = 102
}

// 1.
private val appUpdateManager = AppUpdateManagerFactory.create(context)

fun callThisOnCreate(activity: Activity) {
    val updateInfoTask = appUpdateManager.appUpdateInfo

    appUpdateInfoTask.addOnSuccessListener { updateInfo ->
    // 2.
    if (updateInfo.updateAvailability()!=UpdateAvailability.UPDATE_AVAILABLE) {
        [email protected]
    }

    // 3.
    if (updateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) &&
        MIN_VERSION_CODE_FOR_IMMEDIATE_UPDATE > BuildConfig.VERSION_CODE) {
            startUpdate(activity, appUpdateInfo)
        }
    }
}

fun callThisOnResume(activity: Activity) {
    // 4.
    appUpdateManager
        .appUpdateInfo
        .addOnSuccessListener { appUpdateInfo ->
            if (appUpdateInfo.updateAvailability() 
            == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
                startUpdate(activity, appUpdateInfo)
        }
}

private fun startUpdate(activity: Activity, appUpdateInfo: AppUpdateInfo) {
    // 5.
    appUpdateManager.startUpdateFlowForResult(
        appUpdateInfo,
        AppUpdateType.FLEXIBLE,
        activity,
        REQUEST_CODE_IMMEDIATE)
}

I summarized what you need to get the immediate flow to show. Some explanations below.

  1. Get an AppUpdateManager instance using any Context (even Application Context). For any interaction with the in-app update library, this is the point of contact.
  2. The first thing you need to check after you get an AppUpdateInfo instance (that contains whatever update info the on-device Play Store knows about your app) is if there's an update available. No need to continue if there's no update available. Of course, you can combine (2) and (3).
  3. MIN_VERSION_CODE_FOR_IMMEDIATE_UPDATE is the minimum version code that you allow users to run. If the version code of the app is lower, the full-screen update flow will be shown. This minimum version could be remotely set using a remote config service.
  4. In case the user exits the app and returns while an immediate update takes place, this will resume the update operation.
  5. The actual call to launch the flow. Using that request code you can check that the update was indeed successful and either retry or exit the app otherwise. More details here.

Flexible update flow

companion object {
    const val REQUEST_CODE_FLEXIBLE = 103
}

private val appUpdateManager = AppUpdateManagerFactory.create(context)

fun callThisOnCreate(activity: Activity) {
    val updateInfoTask = appUpdateManager.appUpdateInfo

    appUpdateInfoTask.addOnSuccessListener { updateInfo ->
    if (updateInfo.updateAvailability()!=UpdateAvailability.UPDATE_AVAILABLE) {
        [email protected]
    }

    // 1.
    if (appUpdateInfo.clientVersionStalenessDays() ?: 0 >
          DAYS_FOR_FLEXIBLE_UPDATE
          && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
        startUpdate(activity, appUpdateInfo)
    }
}

private fun startUpdate(activity: Activity, appUpdateInfo: AppUpdateInfo) {
    // 2.
    val listener = { state: InstallState ->
        if (state.installStatus() == InstallStatus.DOWNLOADED) {
            showSomethingWhenUpdateDownloaded(activity)
        }
    }
    appUpdateManager.registerListener(listener)
    
    appUpdateManager.startUpdateFlowForResult(
        appUpdateInfo,
        appUpdateType,
        activity,
        REQUEST_CODE_FLEXIBLE)
}

private fun showSomethingWhenUpdateDownloaded(activity: Activity) {
    activity.showSomeDialog(
        message = "Updated version is downloaded. Install now?",
        yesMessage = "Yes",
        noMessage = "No",
        yesAction = { appUpdateManager.completeUpdate() }
    )
}

A similar process for the flexible update. Some additional explanations below (I assume that you read the comments for the immediate update - won't be repeated here).

  1. The clientVersionStalenessDays() will return how long the on-device Play Store knows about your new update. You can remotely set a DAYS_FOR_FLEXIBLE_UPDATE threshold, after which you can nudge the user to update the app to the latest version.
  2. This is a listener to monitor the flexible update progress. You can show a progress bar if you want while the update is being downloaded. The most important bit though is to notify the user when the app is ready to be updated. This completeUpdate() will restart the app to the latest version.

This just scratched the surface of the In-app Update library. Dig into the docs for more advanced use-cases. Happy coding!

Show Comments