This post shares a brief overview and links to helpful open-source code examples related to the installation process of an Android application using PackageInstaller - the Android team’s recommended method for installation. The post is the second of a series related to the installation process:

  1. How an Android app installation works: Permissions and install by Intent
  2. Installation of an Android app by PackageInstaller (this)
  3. What’s going on inside the OS (coming soon)

Brief overview of the installation process

The API was introduced in Android 5 (API 21) and allows users to streamline installation, updating, and deletion processes. Another important feature is the ability to work with split APKs. This affects developers as well, because many developer-related features, like “Apply changes,” rely on the logic within.

To use PackageInstaller, you need the same permissions for installing, deleting, or updating apps as well as storage permissions. I found it interesting that to get access to already installed applications, you need the REQUEST_PACKAGE_DELETE permission. You can check more details on this in this comment.

In simple cases, the installation process is:

  1. Obtain a session
  2. Copy APK data to the session stream
  3. Commit session changes

One of the parameters to commit a session is a PendingIntent that allows you to get a result or request the user to approve the installation. Here’s a simple example based on the official one:

private fun installAppByActionView() {
        val session = obtainSession()
        pushApkToSession(session)
        session.commit(intentSender())
    }

private fun pushApkToSession(
    session: PackageInstaller.Session,
    path: String = Environment.getExternalStorageDirectory().path + APP_PATH,
) {
    try {
        val file = File(path)
        val fileInputStream = FileInputStream(file)
        val sessionOutputStream = session.openWrite("package", 0, file.length())

        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
        var bytesRead: Int

        while (fileInputStream.read(buffer).also { bytesRead = it } != -1) {
            sessionOutputStream.write(buffer, 0, bytesRead)
        }

        session.fsync(sessionOutputStream)

        fileInputStream.close()
        sessionOutputStream.close()
    } catch (e: IOException) {
        Log.e(TAG, "Error adding APK to session: ${e.message}", e)
    }
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    if (PACKAGE_INSTALLED_ACTION != intent.action) return
    val status: Int = intent.extras?.getInt(PackageInstaller.EXTRA_STATUS) ?: return
    when (status) {
        // system requires approval from the user
        PackageInstaller.STATUS_PENDING_USER_ACTION -> {
            val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
            confirmationIntent?.let { startActivity(confirmationIntent) }
        }

        PackageInstaller.STATUS_SUCCESS -> {
            Log.d(
                TAG,
                "Failed: $status. Message:${intent.extras?.getString(PackageInstaller.EXTRA_STATUS_MESSAGE)}"
            )
        }

        PackageInstaller.STATUS_FAILURE,
        PackageInstaller.STATUS_FAILURE_ABORTED,
        PackageInstaller.STATUS_FAILURE_BLOCKED,
        PackageInstaller.STATUS_FAILURE_CONFLICT,
        PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
        PackageInstaller.STATUS_FAILURE_INVALID,
        PackageInstaller.STATUS_FAILURE_STORAGE -> {
            Log.d(
                TAG,
                "Failed: $status. Message:${intent.extras?.getString(PackageInstaller.EXTRA_STATUS_MESSAGE)}"
            )
        }

        else -> {
            Log.d(
                TAG,
                "Unknown status: $status. Message:${intent.extras?.getString(PackageInstaller.EXTRA_STATUS_MESSAGE)}",
            )
        }
    }
}

In Android 34, you can request user approval before committing by calling:

session.requestUserPreapproval(
            PreapprovalDetails.Builder()
                .setPackageName("xyz.qwexter.installation_demo")
                .setLabel("Approve me")
                //.setIcon()
                //.setLocale()
                .build(),
            intentSender()
        )

There is an interesting difference in the user approval process. When the system requests approval from the user and they deny it, you don’t get any result back. You can only check if the session was abandoned manually or use SessionCallback. But, if you force user approval and they deny it, you’ll receive a STATUS_FAILURE_BLOCKED intent.

If you want to show some progress to the user or react to changes in the installation state, you should use the SessionCallback.

It may seem strange, but I think the best place to get more information is this merge request. I highly recommend checking the logic, which is more advanced than in the examples above. It also covers more cases, like silently installing or updating apps and how to delete them.

And of course F-Droid. I think it’s the best project to get to know installation APIs, check how they manage permissions and UI updates.

Conclusion

This way might seem a bit more complicated compared to using the system installer via Intent. But, it allows you

  1. Customize UI\UX of installation, updating, or deleting process in your app
  2. Silent management of apps in specific situations (check MR for explanation)
  3. Split APKs support

So if you need more advanced tools to install applications that’s your choice.