Painless building of an Android package installer app
Tags: android, androiddev, kotlin, mobile
Published: 2023-11-20 09:17:18

Sometimes you need to install an app on a device. Not as a user, but as a developer of another app. Maybe your app is an app store, or a file manager, or even not any of the above, but you need to self-update and you're not published on Play Store. In any case, you will turn to Android SDK APIs which handle APK installs, and as we all know, Android APIs may often be quite cumbersome to use.

Take APK installs, for instance. If you're unlucky and have to support Android versions below 5.0, you need to use different APIs on different versions of Android: _PackageInstaller_ on versions since 5.0, or some kind of _Intent_ with an install action.

---

Intent.ACTION_INSTALL_PACKAGE way
---------------------------------

_Intent_ is pretty straightforward to use. You just create it, start an Activity for result and handle the returned code. Here is how we handle an install intent using AndroidX Activity Result API:

```kotlin
// registering a launcher in an Activity or Fragment
val installLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    val isInstallSuccessful = result.resultCode == RESULT_OK
    // and then doing anything depending on the result we got
}

// launching an intent, e.g. when clicking on a button
val intent = Intent().apply {
    action = Intent.ACTION_INSTALL_PACKAGE
    setDataAndType(apkUri, "application/vnd.android.package-archive")
    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
    putExtra(Intent.EXTRA_RETURN_RESULT, true)
}
installLauncher.launch(intent)
```

Don't forget to declare an install permission in _AndroidManifest_:

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

Cool. But quite limited (it doesn't support split APKs and doesn't give a reason of installation fail), not even mentioning that this action was deprecated in Android Q in favor of _PackageInstaller_. Also, it doesn't support _content:_ URIs on Android versions below 7.0, and you can't use _file:_ URIs on versions since 7.0 (if you don't want to crash with _FileUriExposedException_). So, in order to correctly handle this on all versions, you need to convert URIs and maybe even create a temporary copy of the file depending on Android version. It becomes not as straightforward as it seemed to be.

---

PackageInstaller way
--------------------

In Android 5.0 Google introduced _PackageInstaller_. It's an API which streamlines installation process, and adds an ability to install split APKs.

_PackageInstaller_ is a lot more robust, and allows to build a full-fledged app store or package manager app. However, with robustness comes complexity.

So, how do we approach installation with _PackageInstaller_?

First, we need to create a _Session_:

```kotlin
val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val packageInstaller = context.packageManager.packageInstaller
val sessionId = packageInstaller.createSession(sessionParams)
val session = packageInstaller.openSession(sessionId)
```

Then, we need to write our APK(s) to it:

```kotlin
apkUris.forEachIndexed { index, apkUri ->
    context.contentResolver.openInputStream(apkUri).use { apkStream ->
        requireNotNull(apkStream) { "$apkUri: InputStream was null" }
        val sessionStream = session.openWrite("$index.apk", 0, -1)
        sessionStream.buffered().use { bufferedSessionStream ->
            apkStream.copyTo(bufferedSessionStream)
            bufferedSessionStream.flush()
            session.fsync(sessionStream)
        }
    }
}
```

After that, we commit the session:

```kotlin
val receiverIntent = Intent(context, PackageInstallerStatusReceiver::class.java)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
    PendingIntent.FLAG_UPDATE_CURRENT
}
val receiverPendingIntent = PendingIntent.getBroadcast(context, 0, receiverIntent, flags)
session.commit(receiverPendingIntent.intentSender)
session.close()
```

What's _PackageInstallerStatusReceiver_? It's a _BroadcastReceiver_ which reacts to installation events. We have to not forget to register it in _AndroidManifest_, as well as declare an install permission:

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

<receiver
    android:name=".PackageInstallerStatusReceiver"
    android:exported="false" />
```

And here's a sample implementation of _PackageInstallerStatusReceiver_:

```kotlin
class PackageInstallerStatusReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
        when (status) {
            PackageInstaller.STATUS_PENDING_USER_ACTION -> {
                // here we handle user's install confirmation
                val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
                if (confirmationIntent != null) {
                    context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
                }
            }
            PackageInstaller.STATUS_SUCCESS -> {
                // do something on success
            }
            else -> {
                val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
                println("PackageInstallerStatusReceiver: status=$status, message=$message")
            }
        }
    }
}
```

Quite a convoluted way to install an app, huh?

---

The third way
-------------

There's another way to launch an install session, that is to use _Intent.ACTION_VIEW_, but I won't cover it here, because it doesn't provide a result of installation, and it doesn't relate to package installation directly.

---

We explored different ways to install an app. But we just scratched the surface. What if we need to handle system-initiated process death? What if we want to get the exact reason why install failed? What if we want to get progress updates while an install session is active? What if we want to defer user's install confirmation via notification? Also, can there be a simpler way to do all of this without worrying about all the details and without writing a lot of code?

Well, there is. I introduce to you the **Ackpine** library.

![Ackpine logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8d16gfej9udsso6yebzg.png)

**Ackpine** is a library providing consistent APIs for installing and uninstalling apps on an Android device. It's easy to use, it's robust, and it provides everything from the above paragraph.

It supports both Java and idiomatic Kotlin with Coroutines integration out of the box.

**Ackpine** uses _Uri_ as a source of APK files, which allows to plug virtually any APK source in via _ContentProviders_, and makes them persistable. The library itself leverages this to provide the ability to install zipped split APKs without extracting them.

---

See the simple example of installing an app in Kotlin with **Ackpine**:

```kotlin
try {
    when (val result = PackageInstaller.getInstance(context).createSession(apkUri).await()) {
        is SessionResult.Success -> println("Success")
        is SessionResult.Error -> println(result.cause.message)
    }
} catch (_: CancellationException) {
    println("Cancelled")
} catch (exception: Exception) {
    println(exception)
}
```

Of course, it's a barebones sample. We need to account for process death, as well as configure the session. Well, the latter is very easy with Kotlin DSLs:

```kotlin
val session = PackageInstaller.getInstance(context).createSession(baseApkUri) {
    apks += apkSplitsUris
    confirmation = Confirmation.DEFERRED
    installerType = InstallerType.SESSION_BASED
    name = fileName
    requireUserAction = false
    notification {
        title = NotificationString.resource(R.string.install_message_title)
        contentText = NotificationString.resource(R.string.install_message, fileName)
        icon = R.drawable.ic_install
    }
}
```

And to handle process death you would write something like this:

```kotlin
savedStateHandle[SESSION_ID_KEY] = session.id

// after process restart
val id: UUID? = savedStateHandle[SESSION_ID_KEY]
if (id != null) {
    val result = packageInstaller.getSession(id)?.await()
    // or anything else you want to do with the session
}
```

Also, **Ackpine** gives you the utilities which make work with zipped split APKs (such as APKS, APKM and XAPK files) a breeze:

```kotlin
val splits = ZippedApkSplits.getApksForUri(zippedFileUri, context) // reading APKs from a zipped file
    .filterCompatible(context) // filtering the most compatible splits
    .throwOnInvalidSplitPackage()
val splitsList = try {
    splits.toList()
} catch (exception: SplitPackageException) {
    println(exception)
    emptyList()
}
```

Receiving installation progress updates is as simple as this:

```kotlin
session.progress
    .onEach { progress -> println("Got session's progress: $progress") }
    .launchIn(someCoroutineScope)
```

So, give it a try and let me hear your feedback!

Check out the [library's repo on GitHub](https://github.com/solrudev/Ackpine), it contains sample projects both in Java and Kotlin.

And the project's website with documentation is [here](https://solrudev.github.io/Ackpine/).

Enjoy your coding!