Search code examples
androidpackageinstaller

Android PackageInstaller installation result


I use PackageInstaller and PackageInstaller.Session to install an app from my app. I would like to display a message in my app when the installation fails (e.g. in case of bad signature of an apk). How can I get result of the installation session?


Solution

  • When you commit() your Session, you need to supply an IntentSender, which points back to some component, such as a BroadcastReceiver. Here, I do that in installCoroutine():

    /*
      Copyright (c) 2019 CommonsWare, LLC
    
      Licensed under the Apache License, Version 2.0 (the "License"); you may not
      use this file except in compliance with the License. You may obtain   a copy
      of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
      by applicable law or agreed to in writing, software distributed under the
      License is distributed on an "AS IS" BASIS,   WITHOUT WARRANTIES OR CONDITIONS
      OF ANY KIND, either express or implied. See the License for the specific
      language governing permissions and limitations under the License.
    
      Covered in detail in the book _Elements of Android Q
    
      https://commonsware.com/AndroidQ
    */
    
    package com.commonsware.q.appinstaller
    
    import android.app.Application
    import android.app.PendingIntent
    import android.content.Intent
    import android.content.pm.PackageInstaller
    import android.net.Uri
    import androidx.documentfile.provider.DocumentFile
    import androidx.lifecycle.AndroidViewModel
    import androidx.lifecycle.viewModelScope
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.withContext
    
    private const val NAME = "mostly-unused"
    private const val PI_INSTALL = 3439
    
    class MainMotor(app: Application) : AndroidViewModel(app) {
      private val installer = app.packageManager.packageInstaller
      private val resolver = app.contentResolver
    
      fun install(apkUri: Uri) {
        viewModelScope.launch(Dispatchers.Main) {
          installCoroutine(apkUri)
        }
      }
    
      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()
          }
        }
    }
    

    Your component can then look at EXTRA_STATUS to get the status:

    /*
      Copyright (c) 2019 CommonsWare, LLC
    
      Licensed under the Apache License, Version 2.0 (the "License"); you may not
      use this file except in compliance with the License. You may obtain   a copy
      of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
      by applicable law or agreed to in writing, software distributed under the
      License is distributed on an "AS IS" BASIS,   WITHOUT WARRANTIES OR CONDITIONS
      OF ANY KIND, either express or implied. See the License for the specific
      language governing permissions and limitations under the License.
    
      Covered in detail in the book _Elements of Android Q
    
      https://commonsware.com/AndroidQ
    */
    
    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")
          }
        }
      }
    }
    

    It looks like STATUS_FAILURE_INVALID will be the one for an invalid signature.