In-App Updates: Getting Started

Learn how to make an in-app mechanism to notify users of a new version of your app. By Carlos Mota.

4.1 (9) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Adding Immediate Updates

When an immediate update is found, Play Core displays an activity on top of your app that blocks all user interactions until the update is accepted or canceled. As such, this type of update is for critical scenarios.

When the user accepts, the system will be responsible for the entire flow from download to installation to the restart. As this flowchart shows:

InAppUpdate-Immediate

From the developer’s vantage, it will only be necessary to start the updating process and respond to the case if the user cancels the update or an error occurs.

After handleUpdate, add the following:

private fun handleImmediateUpdate(manager: AppUpdateManager, info: Task<AppUpdateInfo>) {
  //1
  if ((info.result.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
    //2
    info.result.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) &&
    //3    
    info.result.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
    //4    
    manager.startUpdateFlowForResult(info.result, AppUpdateType.IMMEDIATE, this, REQUEST_UPDATE)
   } 
 }

Here’s a step-by-step rundown of the function:

  • DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS: When there’s an ongoing update.
  • UPDATE_AVAILABLE: When a new update is available.
  • UPDATE_NOT_AVAILABLE: When there’s no update available.
  • UNKNOWN: When there was a problem connecting to the app store.
  1. Before starting the update, it’s important to analyze the response from the Play Store. The update availability can be one of the following values:
  2. Before starting this process, verify that there’s an update available or one already in progress.
  3. Verify if the immediate type is supported.
  4. Start or resume the update with the startUpdateFlowForResult but only if the previous conditions are true.

You’ll need to set a request code after calling startUpdateFlowForResult which will launch an external activity. That way, when it finishes, you can check if the operation proved successful or not.

Add the constant below to MainActivity before the class declaration:

private const val REQUEST_UPDATE = 100

After calling startUpdateFlowForResult with AppUpdateType.IMMEDIATE, the Play Core activity will take care of the update and restart the app when it finishes.

You’ll see a progress bar with the current state of the update and how long it will take to finish. Hang on for a second, you’ll be testing the app soon. Before that, follow carefully to implement the flow for flexible updates.

Adding Flexible Updates

A flexible update is not as intrusive as an immediate one. You decide how to notify users about the new version, and you have more control over its flow. For Today’s Weather app, you’ll use a button. As the flowchart below demonstrates:

InAppUpdate-Flexible

Before defining the handleFlexibleUpdate, add these two views inside RelativeLayout on activity_main.xml:

<TextView
   android:id="@+id/tv_status"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_margin="16dp"
   android:layout_centerHorizontal="true"
   android:visibility="gone"
   android:fontFamily="sans-serif-condensed-medium"
   android:text="@string/info_processing"
   android:textSize="20sp" />

<Button
   android:id="@+id/btn_update"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_below="@id/tv_status"
   android:layout_centerHorizontal="true"
   android:visibility="gone"
   android:text="@string/action_update"
   android:textSize="20sp" />

TextView displays the current state of the update, and the Button triggers this action. As you can see, both views’ visibility is set as gone. They should only become visible if there’s a flexible update available.

Add handleFlexibleUpdate after handleImmediateUpdate which is responsible for implementing the flexible update type.

private fun handleFlexibleUpdate(manager: AppUpdateManager, info: Task<AppUpdateInfo>) {
  if ((info.result.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
    info.result.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) &&
    info.result.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
      btn_update.visibility = View.VISIBLE
      setUpdateAction(manager, info)
    }
}

Similar to immediate updates, flexible updates first check for available updates. If one is found, the button to start the process becomes visible, and the app calls setUpdateAction to handle the flow.

However, with flexible updates, there’s no automatic update flow. The app needs to implement it.

To achieve this, first add the following before onCreate:

private lateinit var updateListener: InstallStateUpdatedListener

Then add the function below to MainActivity after handleFlexibleUpdate declaration:

private fun setUpdateAction(manager: AppUpdateManager, info: Task<AppUpdateInfo>) {
  //1
  btn_update.setOnClickListener {
    //2
    updateListener = InstallStateUpdatedListener {
      //3
      btn_update.visibility = View.GONE
      tv_status.visibility = View.VISIBLE
      //4
      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)
        }
        //5
        InstallStatus.DOWNLOADED -> {
          tv_status.text = getString(R.string.info_installing)
          launchRestartDialog(manager)
        }
        InstallStatus.INSTALLING -> {
          tv_status.text = getString(R.string.info_installing)
        }
        //6
        InstallStatus.INSTALLED -> {
          tv_status.text = getString(R.string.info_installed)
          manager.unregisterListener(updateListener)
        }
        else -> {
          tv_status.text = getString(R.string.info_restart)
        }
      }
    }
    //7
    manager.registerListener(updateListener)
    //8
    manager.startUpdateFlowForResult(info.result, AppUpdateType.FLEXIBLE, this, REQUEST_UPDATE)
  }
}

Here’s a step-by-step code analysis:

  • If on foreground, the user needs to confirm that the app can be relaunched. This avoids interrupting their current usage of the app.
  • If on background, the user minimizes it after declining the installation. The system will automatically install the newest update and relaunch when the app returns to foreground.
  1. First, it sets the callback for when the user taps the button. This action will start the update action defined in step 7.
  2. It defines InstallStateUpdateListener, which notifies the app of every step of the process.
  3. tv_status displays visual information about the update. It’s hidden by default.
  4. The when block defines all the possible states on the update flow.
  5. When the system finishes downloading the .apk, the app can either be in one of two states:
  6. After the update is successfully installed, there’s no need to keep the listener register in the app. There are no more actions to be made, so you can hide the Button and unregister the callback.
  7. Register the previous listener.
  8. Start the flexible update.

When the installation is completed, implement a dialog to inform the user that the newest version of the app is ready to be installed. For that, it needs to be relaunched. Add the following after the setUpdateAction function.

private fun launchRestartDialog(manager: AppUpdateManager) {
  AlertDialog.Builder(this)
    .setTitle(getString(R.string.update_title))
    .setMessage(getString(R.string.update_message))
    .setPositiveButton(getString(R.string.action_restart)) { _, _ ->
      manager.completeUpdate()
    }
    .create().show()
}

Add the function below after setUpdateAction:

AppUpdateManager.completeUpdate()

When the user clicks on the dialog button, the app will restart.

Handling User Actions

Since it’s possible to cancel an update, it’s important to override onActivityResult and check the resultCode received so the app can respond accordingly.

This scenario is valid for both types of updates. Add the following block of code after the launchRestartDialog function:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  //1
  if (REQUEST_UPDATE == requestCode) {
    when (resultCode) {
      //2
      Activity.RESULT_OK -> {
        if (APP_UPDATE_TYPE_SUPPORTED == AppUpdateType.IMMEDIATE) {
          Toast.makeText(baseContext, R.string.toast_updated, Toast.LENGTH_SHORT).show()
        } else {
          Toast.makeText(baseContext, R.string.toast_started, Toast.LENGTH_SHORT).show()
        }
      }
      //3
      Activity.RESULT_CANCELED -> {
        Toast.makeText(baseContext, R.string.toast_cancelled, Toast.LENGTH_SHORT).show()
      }
      //4
      ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> {
        Toast.makeText(baseContext, R.string.toast_failed, Toast.LENGTH_SHORT).show()
      }
   }
  super.onActivityResult(requestCode, resultCode, data)
 }
}

In this scenario, you’ll just show a toast depending on the user action:

  1. Confirm that onActivityResult was called with requestCode from an in-app update. It should be the same code as the one defined in startUpdateFlowForResultREQUEST_UPDATE.
  2. The update was successfully installed.
  3. The user canceled the update. Although it’s showing a toast, as a challenge, you can show a dialog mentioning the importance of always installing the latest version.
  4. The update failed due to some unknown reason. Typically, this is an error from the Play Store. Try to restart the update process.