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
:
- A
PackageInstaller
instance, obtained by requesting one fromPackageManager
- A
ContentResolver
instance
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:
- Call
openWrite()
on theSession
to get anOutputStream
- Copy the bytes from our
InputStream
to thatOutputStream
- Call
fsync()
on theSession
to say “we’re done, please ensure everything is written to disk”
The three parameters to openWrite()
are:
- Some seemingly arbitrary “name” string
- The offset into the bytes that the
Session
should start using (typically pass0
) - The number of bytes that will need to be read in, or
-1
if you do not know the length
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:
- It succeeds
- It fails for some reason (e.g., duplicate
ContentProvider
authority conflict) - The user needs to approve the installation
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:
-
com.android.packageinstaller.notification.smallIcon
, pointing to a drawable resource representing your desired icon -
com.android.packageinstaller.notification.color
, pointing to a color resource for your desired tint on that icon (presumably)
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.