Exploring in-app updates on Android

jp

I’m sure there has often been a time when you’ve needed to send out an app update that has some form of urgency – maybe there’s a security issue or some bug which is causing a lot of issues for users. Previously, 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. At Google I/O this week we saw the announcement of In-App Updates for the Play Core library. In this post we’re going to learn more about this addition and how we can make use of it in our applications.


Supporting API level 21 and above, the Play Core library now allows us to offer in-app updates to our users – meaning we can show that an app update is available whilst the user is within the context of our application. 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.

update

The Flexible approach shows the user an upgrade dialog but performs the downloading of the update within the background. This means that the user can continue using our app whilst the update is being downloaded. For more crucial application updates we can make use of the Immediate flow – this is where a blocking UI is used to prompt the user to update the application, disallowing continued use until the app has been updated and restarted.


Checking an update is available

Before we can get started with either of these, we’re going to begin by checking whether there is an update that is available from the play store. The code for doing this looks like so:

val updateManager = AppUpdateManagerFactory.create(this)
updateManager.appUpdateInfo.addOnSuccessListener {
    if (it.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
        it.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {

    }
}

We begin by creating an instance of the AppUpdateManager class – this will be responsible for handling the information around our application information. Using this, we’re going to fetch an AppUpdateInfo instance – this needs to make a remote request to do so, which is why you can see us accessing the result property above. This AppUpdateInfo contains a collection of data that can be used to determine whether we should trigger the update flow.

To begin with, it has a method availableVersionCode() – if there is an update that is either currently available or in progress of being updated, this will return that version value. As well as this, there is also an updateAvailability() method which returns a value that represents the update state. This can be either:

  • UNKNOWN
  • UPDATE_AVAILABLE
  • UPDATE_IN_PROGRESS
  • DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS

We first want to check that this is equal to UPDATE_AVAILABLE, followed by ensuring that the desired update type is supported by using the isUpdateTypeAllowed() function – passing in AppUpdateType type (either IMMEDIATE or FLEXIBLE) to ensure that the update type we want to use is supported.

Now that we have the information we need to determine whether or not an app update is available, we’re going to want to trigger the update flow. We can do this by making use of the startUpdateFlowForResult() method that comes with the AppUpdateManager class. When we call this we just need to pass:

  • The AppUpdateInfo instance that we previously retrieved
  • The AppUpdateType that we want to trigger (IMMEDIATE or FLEXIBLE)
  • The context for the current component
  • A request code so that cancellations / failures can be caught within the calling component
updateManager.startUpdateFlowForResult(
    appUpdateInfo,
    AppUpdateType.IMMEDIATE,
    this,
    REQUEST_CODE_UPDATE)

Calling this startUpdateFlowForResult() method will trigger a startActivityForResult() call and kick off the app update flow. In some cases, the request app update may be cancelled by the user (ActivityResult.RESULT_CANCELLED), or even fail (ActivityResult.RESULT_IN_APP_UPDATE_FAILED). In these cases we can catch the result in the onActivityResult() of our activity / fragment and handle the result accordingly.

override fun onActivityResult(
    requestCode: Int, 
    resultCode: Int, 
    data: Intent
) {
    if (requestCode === REQUEST_CODE_UPDATE) {
        if (requestCode != RESULT_OK) {
        
        }
    }
}

Immediate in-app updates

Immediate in-app updates can be triggered by using the AppUpdateType.IMMEDIATE value – and as previously mentioned, this will trigger a blocking UI flow for the user until they have updated the app. This means that this UI will be shown during the entire time that the app is downloading and then installing, until the entire update process has completed. When the user leaves your app whilst the update is in process, the update will continue to download and then install in the background. However, if the user leaves and then returns to your app before the update process has completed, then you will need to ensure that we continue the update process.

immediate

For this we need to check whether or not the updateAvailability() returns the DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS state. If so then we need to trigger the update flow so that the update process can be resumed. If you do not implement this part of the flow then the user will be able to continue using your application without the immediate update in-effect.

override fun onResume() {
    super.onResume()
    val updateManager = AppUpdateManagerFactory.create(this)
    updateManager.appUpdateInfo
        .addOnSuccessListener {
            if (it.updateAvailability() == 
                UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
                updateManager.startUpdateFlowForResult(
                    it,
                    IMMEDIATE,
                    this,
                    REQUEST_CODE_UPDATE)
            }
        }
    }
}

For immediate updates we are not required to do any further work. The implementation will automatically restart our app once the update is downloaded, this is so that the update can be installed.


Flexible in-app updates

Flexible in-app updates can be triggered by using the AppUpdateType.FLEXIBLE value – and as previously mentioned, this will trigger a flow that displays an upgrade pop-up to the user and perform a download / install of the update in the background whilst the user can continue to use the application.

fleixble

For this, we begin by launching the app update flow for a FLEXIBLE update:

updateManager.startUpdateFlowForResult(
    appUpdateInfo, 
    AppUpdateType.FLEXIBLE, 
    this, 
    REQUEST_CODE_UPDATE)

Because this is all happening in the background, rather than creating a blocking UI like the immediate update, we need to add some monitoring so that we can keep a check on the state of the update install. For this we’re going to make use of the InstallStateUpdatedListener which will receive callbacks when the state of the flexible install changes. This contains a single callback, onStateUpdated(), which will pass us an instance of the InstallState class. From this we can make use of:

  • installStatus() – returns us a InstallStatus value that represents the current state of the update. This can be one of:
    • UNKNOWN
    • REQUIRES_UI_INTENT
    • PENDING
    • DOWNLOADING
    • DOWNLOADED
    • INSTALLING
    • INSTALLED
    • FAILED
    • CANCELLED
  • installErrorCode() – returns us an InstallErrorCode that represents the error state of the install
    • NO_ERROR
    • NO_ERROR_PARTIALLY_ALLOWED
    • ERROR_UNKOWN
    • ERROR_API_NOT_AVAILABLE
    • ERROR_INVALID_REQUEST
    • ERROR_INSTALL_UNAVAILABLE
    • ERROR_INSTALL_NOT_ALLOWED
    • ERROR_DOWNLOAD_NOT_PRESENT
    • ERROR_INTERNAL_ERROR
  • packageName() – returns us the package name that the install status applies to

Whilst there are a lot of options here for both the install status and the install error code, this gives us a lot of flexibility when it comes to handling the status. For example, if we are showing the user some specific UI to notify them of the current state, then we can customise the content within this based on the current status.

val listener = InstallStateUpdatedListener {
    // Handle install state
}

We can also have our activity implement this interface so that the callback can be overriden within the activity itself.

Now that we have the listener, we can register it with out AppUpdateManager instance using the registerListener() method. When finished with listener, we need to be sure to make use of the unregisterListener() method to remove any callbacks being triggered when they are no longer required.

val updateManager = AppUpdateManagerFactory.create(this)
updateManager.registerListener(listener)

...

updateManager.unregisterListener(listener)

Once we detect that the InstallStatus represents the DOWNLOADED state, we are required to restart the app so that the update can be installed. Whilst the immediate update method handles this for you, in the case of the flexible update we need to manually trigger this. In order to manually trigger this update we need to make use of the completeUpdate() method from our AppUpdateManager instance. When this is called, a full-screen UI will be displayed from the play core library and the app will be restarted in the background so that the update can be installed. The app will then be restarted with the update applied.

appUpdateManager.completeUpdate()

When this is called from the background, the app will still be updated but no fullscreen UI will be displayed and the app will not be relaunched once the update has been completed.

However, if the update is being carried out when the app is in the foreground then there is a chance that the user will leave and return to our app before the update has been downloaded and installed. In this case, when our activity hit onResume() we’re going to want to check the status from our AppUpdateManager so that we can determine if we need to complete the update process. We can determine this by checking if the InstallStatus is in a DOWNLOADED state. If so, we can go ahead and call the completeUpdate() method to finish the update process.

updateManager.appUpdateInfo
    .addOnSuccessListener {appUpdateInfo ->
        if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
            updateManager.completeUpdate()
        }
    }

From this article we’ve learnt about the new approach to in-app updates that is provided by the play core library. I’m excited to use this in the applications that I work on and I really believe that this will greatly improve a range of experiences for both our users and developers when it comes to android application development. If you have any questions about the play core library or in-app updates then please do reach out.

In my next article i’ll be talking about the CameraX Jetpack library on Android. Follow me on Twitter to keep updated as to when this is released!

[twitter-follow screen_name=’hitherejoe’]