androidsessionapkandroid-package-managerspackageinstaller

What's "PackageInstaller" class on Lollipop, and how to use it?


Background

I've noticed there is a new function on the PackageManager called "getPackageInstaller" , with minAPI 21 (Lollipop).

I've reached the "PackageInstaller" class, and this is what it is written about it:

Offers the ability to install, upgrade, and remove applications on the device. This includes support for apps packaged either as a single "monolithic" APK, or apps packaged as multiple "split" APKs.

An app is delivered for installation through a PackageInstaller.Session, which any app can create. Once the session is created, the installer can stream one or more APKs into place until it decides to either commit or destroy the session. Committing may require user intervention to complete the installation.

Sessions can install brand new apps, upgrade existing apps, or add new splits into an existing app.

Questions

  1. What is this class used for? Is it even available for third party apps (I don't see any mentioning of this) ?
  2. Can it really install apps?
  3. Does it do it in the background?
  4. What are the restrictions?
  5. Does it require permissions? If so, which?
  6. Is there any tutorial of how to use it?

Solution

  • OK, I've found some answers:

    1. Can be used for installing/updating APK files, including split-APK files. Maybe even more.
    2. Yes, but the user will need to confirm, one app after another.
    3. Maybe if the app is built in.
    4. Seems it requires to read the entire APK file/s before requesting the user to install.
    5. Needs permission REQUEST_INSTALL_PACKAGES
    6. Haven't found any, but someone showed me here how to install split-apk files, and here's how to do it for a single file using SAF, with and without PackageInstaller. Note that this is just a sample. I don't think it's a good practice to do it all on the UI thread.

    manifest

    <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
              package="com.android.apkinstalltest">
        <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
    
        <application tools:ignore="AllowBackup,GoogleAppIndexingWarning"
                     android:allowBackup="true"
                     android:icon="@mipmap/ic_launcher"
                     android:label="@string/app_name"
                     android:roundIcon="@mipmap/ic_launcher_round"
                     android:supportsRtl="true"
                     android:theme="@style/AppTheme">
            <activity
                    android:name=".MainActivity"
                    android:label="@string/app_name"
                    android:theme="@style/AppTheme.NoActionBar">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>
    
                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
    
            <service android:name=".APKInstallService"/>
    
        </application>
    
    </manifest>
    

    APKInstallService

    class APKInstallService : Service() {
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
            when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
                PackageInstaller.STATUS_PENDING_USER_ACTION -> {
                    Log.d("AppLog", "Requesting user confirmation for installation")
                    val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
                    confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    try {
                        startActivity(confirmationIntent)
                    } catch (e: Exception) {
                    }
                }
                PackageInstaller.STATUS_SUCCESS -> Log.d("AppLog", "Installation succeed")
                else -> Log.d("AppLog", "Installation failed")
            }
            stopSelf()
            return START_NOT_STICKY
        }
    
        override fun onBind(intent: Intent) = null
    }
    

    MainActivity

    class MainActivity : AppCompatActivity() {
        private lateinit var packageInstaller: PackageInstaller
    
        @TargetApi(Build.VERSION_CODES.O)
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            setSupportActionBar(toolbar)
            packageInstaller = packageManager.packageInstaller
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
            intent.addCategory(Intent.CATEGORY_OPENABLE)
            intent.type = "application/vnd.android.package-archive"
            startActivityForResult(intent, 1)
        }
    
    //    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
    //        super.onActivityResult(requestCode, resultCode, resultData)
    //        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && requestCode == 1 && resultCode == Activity.RESULT_OK && resultData != null) {
    //            val uri = resultData.data
    //            grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    //            val intent = Intent(Intent.ACTION_INSTALL_PACKAGE)//
    //                    .setDataAndType(uri, "application/vnd.android.package-archive")
    //                    .putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
    //                    .putExtra(Intent.EXTRA_RETURN_RESULT, false)
    //                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    //            startActivity(intent)
    //        }
    
        override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
            super.onActivityResult(requestCode, resultCode, resultData)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && requestCode == 1 && resultCode == Activity.RESULT_OK && resultData != null) {
                val uri = resultData.data ?: return
                grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
                var cursor: Cursor? = null
                var outputStream: OutputStream? = null
                var inputStream: InputStream? = null
                var session: PackageInstaller.Session? = null
                try {
                    cursor = contentResolver.query(uri, null, null, null, null)
                    if (cursor != null) {
                        cursor.moveToNext()
                        val fileSize = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE))
                        val fileName = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
                        installParams.setSize(fileSize)
                        cursor.close()
                        val sessionId = packageInstaller.createSession(installParams)
                        Log.d("AppLog", "Success: created install session [$sessionId] for file $fileName")
                        session = packageInstaller.openSession(sessionId)
                        outputStream = session.openWrite(System.currentTimeMillis().toString(), 0, fileSize)
                        inputStream = contentResolver.openInputStream(uri)
                        inputStream.copyTo(outputStream)
                        session.fsync(outputStream)
                        outputStream.close()
                        outputStream = null
                        inputStream.close()
                        inputStream = null
                        Log.d("AppLog", "Success: streamed $fileSize bytes")
                        val callbackIntent = Intent(applicationContext, APKInstallService::class.java)
                        val pendingIntent = PendingIntent.getService(applicationContext, 0, callbackIntent, 0)
                        session!!.commit(pendingIntent.intentSender)
                        session.close()
                        session = null
                        Log.d("AppLog", "install request sent. sessions:" + packageInstaller.mySessions)
                    }
                } catch (e: Exception) {
                    Log.d("AppLog", "error:$e")
                } finally {
                    outputStream?.close()
                    inputStream?.close()
                    session?.close()
                    cursor?.close()
                }
            }
        }
    }