Installing Apps Using PackageInstaller

In the beginning, to install an APK, you would use an ACTION_VIEW Intent, with a file Uri pointing to the APK. Pass that to startActivity(), and Android would take over from there.

This process evolved over the years, such as adding ACTION_INSTALL_PACKAGE in Android 4.0 and adding content Uri support in Android 7.0. A PackageInstaller class was added in Android 5.0, but it seemed complicated, so a lot of developers stuck with the earlier Intent-based solutions.

However, ACTION_INSTALL_PACKAGE was deprecated in API Level 29, with a request that we use PackageInstaller instead. While not specifically deprecated, one imagines that ACTION_VIEW is also frowned upon for installing apps.

PackageInstaller is designed for more complex scenarios, including dealing with split APKs, where a single app might require more than one APK to completely install. As a result, it has a convoluted API, to go along with the typical skimpy documentation.

So, in this chapter, we will examine how to use PackageInstaller to install a simple APK, for a functional equivalent to the deprecated ACTION_INSTALL_PACKAGE.

Note that ACTION_UNINSTALL_PACKAGE was also deprecated in Android 10. However, while PackageInstaller has a pair of uninstall() methods, these cannot be used by ordinary apps.

Applying PackageInstaller

The AppInstaller sample module in the book’s sample project has a stub activity with an “open” action bar item. Clicking that will open the standard ACTION_OPEN_DOCUMENT content picker UI, for you to find an APK to install. If you select an APK, the app then uses PackageInstaller to install that APK, with a bit of an assist from you as the user.

Permissions

Android 6.0 debuted the REQUEST_INSTALL_PACKAGES permission, and Android 8.0 started enforcing it for apps using ACTION_INSTALL_PACKAGE. Not surprisingly, you need it for PackageInstaller as well. This is a normal permission, so you do not need to request it at runtime — just have the <uses-permission> element in the manifest:

  <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

Creating and Using a Session

The AppInstaller app uses the same sort of architecture pattern as seen in several of the other samples, where we have a ViewModel implementation called MainMotor that our UI layer uses. In this case, MainActivity calls an install() function on MainMotor, handing over the Uri that it received from the ACTION_OPEN_DOCUMENT request.

MainMotor actually is an AndroidViewModel, as we need two things tied to a Context:

  private val installer = app.packageManager.packageInstaller
  private val resolver = app.contentResolver

Unlike ACTION_INSTALL_PACKAGE, we need to do our own I/O to install APKs using PackageInstaller. So, install() in MainMotor turns around and calls an installCoroutine() function, launched from viewModelScope:

  fun install(apkUri: Uri) {
    viewModelScope.launch(Dispatchers.Main) {
      installCoroutine(apkUri)
    }
  }

installCoroutine(), in turn, is a suspend function that wraps its work in a withContext(Dispatchers.IO) block, to have our I/O be performed on a background thread:

  private suspend fun installCoroutine(apkUri: Uri) =
    withContext(Dispatchers.IO) {
      resolver.openInputStream(apkUri)?.use { apkStream ->
        val length =
          DocumentFile.fromSingleUri(getApplication(), apkUri)?.length() ?: -1
        val params =
          PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
        val sessionId = installer.createSession(params)
        val session = installer.openSession(sessionId)

        session.openWrite(NAME, 0, length).use { sessionStream ->
          apkStream.copyTo(sessionStream)
          session.fsync(sessionStream)
        }

        val intent = Intent(getApplication(), InstallReceiver::class.java)
        val pi = PendingIntent.getBroadcast(
          getApplication(),
          PI_INSTALL,
          intent,
          PendingIntent.FLAG_UPDATE_CURRENT
        )

        session.commit(pi.intentSender)
        session.close()
      }
    }

First, we get an InputStream on the content identified by the Uri and use DocumentFile to find out the length of that content.

Then, we create and open a PackageInstaller.Session. To create a session, we call createSession() on PackageManager, providing a PackageInstaller.SessionParams object as a parameter. Most of the time, you will use MODE_FULL_INSTALL as the type of session that we want, to install an app from scratch — there is also a MODE_INHERIT_EXISTING to add new split APKs to an already-installed app. createSession() does not give us the Session object, though — we get an Int identifier instead, and we need to call openSession() to get the actual Session.

With ACTION_INSTALL_PACKAGE, we provided a Uri that pointed to the APK to install. With PackageInstaller, instead, we need to provide the bytes of that APK manually. And, instead of us just passing an InputStream to PackageInstaller.Session, we have a more complex API:

The three parameters to openWrite() are:

We then call commit() and close() on the Session to request the actual install to occur. commit() takes an IntentSender object — typically you get one of these by calling getIntentSender() on some PendingIntent that you create.

Getting the Results

Roughly speaking, there are three possible outcomes of our request:

For an ordinary app, that third outcome will always happen, en route to some final success or failure state.

We find out about all of this via the PendingIntent that we set up. In this case, that pointed to an InstallReceiver, a manifest-registered BroadcastReceiver that will be invoked when needed:

package com.commonsware.q.appinstaller

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.media.AudioManager
import android.media.ToneGenerator
import android.util.Log

private const val TAG = "AppInstaller"

class InstallReceiver : BroadcastReceiver() {
  override fun onReceive(context: Context, intent: Intent) {

    when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
      PackageInstaller.STATUS_PENDING_USER_ACTION -> {
        val activityIntent =
          intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)

        context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
      }
      PackageInstaller.STATUS_SUCCESS ->
        ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100)
          .startTone(ToneGenerator.TONE_PROP_ACK)
      else -> {
        val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)

        Log.e(TAG, "received $status and $msg")
      }
    }
  }
}

We find out which of those scenarios occurs via the PackageInstaller.EXTRA_STATUS extra on the Intent delivered to the component. This is an Int value that will correspond to one of a set of STATUS_ constants on PackageInstaller.

When we get the PackageInstaller.STATUS_PENDING_USER_ACTION status, we can get a pre-populated Intent from the Intent.EXTRA_INTENT extra on the Intent that we received. We can then use that with startActivity() to bring up system dialogs for the user to confirm that they want us to be able to install apps and they want this particular app to be installed. Note, though, that since we are calling startActivity() from onReceive() of a BroadcastReceiver, we need to add FLAG_ACTIVITY_NEW_TASK to be able to start the activity.

If we get PackageInstaller.STATUS_SUCCESS, then the APK was successfully installed. This app simply plays an acknowledgment tone via ToneGenerator, but a more sophisticated app would update its UI, display a Notification, or something.

Any other status code indicates some type of error condition, such as “this app is already installed with the same or higher version” (STATUS_FAILURE_CONFLICT) or “this app is incompatible with the device” (STATUS_FAILURE_INCOMPATIBLE). A human-readable status message should be in the PackageInstaller.EXTRA_STATUS_MESSAGE extra. This sample app just logs that information to Logcat, but a more sophisticated app might have some sort of error state in the UI, such as an error dialog.

System Notification, Maybe

In theory, the system is supposed to display a notification on Android 10 devices after the app is installed. In practice, that is not working. If someday it starts working, though, there are two <meta-data> elements that you can add to tailor the icon for that notification:

The AppInstaller app customizes the icon but leaves the color alone:

    <meta-data
      android:name="com.android.packageinstaller.notification.smallIcon"
      android:resource="@drawable/ic_install_notification" />

However, in the short term, you can ignore those <meta-data> elements, as they have no effect.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.