While researching how Google Play Integrity API can potentially check the installation source, I found the installation process quite interesting. So I decided to dive into details of how we can install apps and how the operating system (OS) installs them inside. There will be a few parts:

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

To simplify, I suggest imagining a situation: we are working on “the App”, and one of the requested features is to install an app. I’d like to skip the download part and assume the APK is already on the device. We have several ways to solve it:

  1. Fully delegate to the system installation process (non-session install)
  2. Manage the installation process in “the App” ourselves (session install)

But before we start the installation, we need to talk about permissions.

Permissions

We have two main permissions related to the installation process (other related to deletion and updates):

  • INSTALL_PACKAGES - Allow install apps without user interaction, completely silent process. Used by the system-level apps or the device manager that needs to install packages without user interaction (e.g., for enterprise solutions or OEMs) and it’s nearly impossible to launch the app with this permission in Google Play. Even more, Google Play protects can delete apps with this permission even if you download and install them from other sources.
  • REQUEST_INSTALL_PACKAGES - Allow request an installation process to the user. Requires user interaction and can install an app silently in some cases. We must use this one to install something, and be ready to explain why you need this permission to the Google Play team or your publication can be rejected.

Note: It’s interesting but my demo app with INSTALL_PACKAGES permission was scanned during installation by Play Protect and by manual trigger, and the demo app survived both checks on my personal device. It seems you need something else to be deleted, not only the permission.

Starting from Android 8 you also need the granted by user permission for installation from unknown sources even if you declare REQUEST_INSTALL_PACKAGES in your manifest.

By the way, we need some permissions to access storage too. It depends on the APK location and I think it’s better to check official documentation regardless of all API changes.

Note: Some of the official examples don’t work as expected due to outdated work with storage permissions. You can check some API Demos on the AOSP emulator.

Add to AndroidManifest.xml:

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

When we get all the necessary permissions we allow installation from an Unknown source. What’s next? As I mentioned earlier, we have two ways, but now we describe the simplest one - delegate it to OS.

Delegate it to Android OS - launch intent.

Sounds easy, right? And in general, it is. But you know, that’s Android SDK with a relatively long history. Android allows you to launch 2 intents to start installation:

  • Intent.ACTION_VIEW - yes, this one can install apps too. (I’ve seen a mention that this way deprecated too, but I didn’t find off sources if you have a link, let me know). This way is the simplest and limited from any configuration point of view.
  • Intent.ACTION_INSTALL_PACKAGE - it is marked deprecated but still works (the latest API that I tested was API 34). In this case, we have more ways to control installations, like marking that app installed not from an unknown source, or installing the same app that exists in the system for another user. Also if you setup the installer name as your app, you can receive bug reports extras by Intent.ACTION_APP_ERROR.
fun installByActionInstallPackage(uri: Uri) {
    Intent().apply {
        action = Intent.ACTION_INSTALL_PACKAGE
        setData(uri)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        startActivity(this)
    }
}

fun installByViewIntent(uri: Uri) {
    Intent().apply {
        action = Intent.ACTION_VIEW
        // or change mime type to "*/*", for some reason this works better on newer Android versions
        setDataAndType(uri, "application/vnd.android.package-archive")
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        startActivity(this)
    }
}

That’s all code and idea.

I’ve found some discussions on StackOverflow like this when developers shared problems with delegation installation, but it looks more like a problem with access to APK file and storage permissions changes either deprecation.

If you are looking for some example:

  1. very friendly official example (in Java), but without proper storage access
  2. File managers like Fossify File Manager or Amaze File Manager)
  3. Firebase App Distribution uses the same approach too.
  4. And F-Droid too!

Conclusion

What we can say about this way?

  • It’s extremely simple
  • The configuration is very limited
  • It’s outdated in some way and the Android team recommends usage session installation

But still helpful if you want to install a simple app.