19

My 3rd party app has a way for the end-user to download an updated APK from our server and then the app will invoke the install package manager on that APK after it's done downloading. This same method has worked for all versions of Android OS but now it will crash on Android 10 (api 29). I haven't seen anyone with a similar problem yet, any help would be greatly appreciated!

Here's what I use to call the APK file from within my app:

Intent intent = new Intent(Intent.ACTION_VIEW);
final File apkFile = new File(Files.getApkFileName());
Log.v("dt.update", "Start update from " + apkFile.getAbsolutePath());
intent.setDataAndType(Uri.fromFile(apkFile), application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

And here is the stacktrace coming back every time, only on Android 10 / API29:

E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.MyAppHere, PID: 11107
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=file:///storage/emulated/0/Download/updatedapp.apk typ=application/vnd.android.package-archive flg=0x10000000 }
    at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2051)
    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1709)
    at android.app.Activity.startActivityForResult(Activity.java:5192)
    at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:676)
    at android.app.Activity.startActivityForResult(Activity.java:5150)
    at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:663)
    at android.app.Activity.startActivity(Activity.java:5521)
    at android.app.Activity.startActivity(Activity.java:5489)
    at android.view.View.performClick(View.java:7140)
    at android.view.View.performClickInternal(View.java:7117)
    at android.view.View.access$3500(View.java:801)
    at android.view.View$PerformClick.run(View.java:27351)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
buradd
  • 1,271
  • 1
  • 13
  • 19
  • Are you running this code in a service? Could it be related to https://developer.android.com/guide/components/activities/background-starts? – stkent Sep 24 '19 at 18:12
  • coincidentally, it is being ran from a service, however i get the same exact exception even when running the same code from the mainactivity directly and even when using an instance of the mainactivity within the service – buradd Sep 24 '19 at 18:27
  • Hmm. So presumably there is just no activity available to handle the intent. What activity usually responds on earlier versions of Android? – stkent Sep 24 '19 at 18:37
  • It usually acts the same as if you were to have clicked on the APK file in your file explorer, it brings up the option to install this APK or cancel – buradd Sep 24 '19 at 18:46

1 Answers1

19

ACTION_VIEW (for APKs) and ACTION_INSTALL_PACKAGE were deprecated in Android 10. You need to switch to the PackageInstaller API.

This sample app demonstrates the basics for getting a simple APK installed. The guts are in the MainMotor:

/*
  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(application, 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(application, InstallReceiver::class.java)
        val pi = PendingIntent.getBroadcast(
          application,
          PI_INSTALL,
          intent,
          PendingIntent.FLAG_UPDATE_CURRENT
        )

        session.commit(pi.intentSender)
        session.close()
      }
    }
}

When an activity or fragment calls install(), supplying a Uri to the APK, I use PackageInstaller to install it:

  • Get a PackageInstaller from PackageManager
  • Create a SessionParams and open a session from it
  • Write the bytes of the APK (read from an InputStream from the Uri) to an OutputStream supplied by that session
  • Call commit() to actually begin the installation process, with results being delivered back to the app via a PendingIntent
  • Call close() to close up the session

The API is clunky, but it is designed to handle a wide range of scenarios, including "App Bundle"-style multi-APK installations.

Abandoned Cart
  • 4,512
  • 1
  • 34
  • 41
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 4
    I came back to answer this question myself as I found the answer and implemented the fix while cross-referencing Google's APIDemos, the answer is the exact same as yours, so I've accepted the answer, but the Kotlin example makes me cringe :( I am only interested in Java and it was great to find the sample written in Java by Google: https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/content/InstallApkSessionApi.java – buradd Sep 25 '19 at 21:05
  • 3
    Is there any way to show the old update/install flow like the same way if an intent with the action `Intent.ACTION_INSTALL_PACKAGE` is started? My point is that using the PackageInstaller won't show the install update progress to the user instead it will silently update and close the app without informing him. – mathew11 Dec 17 '19 at 12:04
  • 2
    @mathew11: AFAIK, you would be responsible for that UI. See https://developer.android.com/reference/kotlin/android/content/pm/PackageInstaller.SessionCallback#onProgressChanged(kotlin.Int,%20kotlin.Float) – CommonsWare Dec 17 '19 at 12:47
  • @CommonsWare Is it necessary to send the broadcast ? `val intent = Intent(getApplication(), InstallReceiver::class.java) val pi = PendingIntent.getBroadcast( getApplication(), PI_INSTALL, intent, PendingIntent.FLAG_UPDATE_CURRENT) session.commit(pi.intentSender)` – rupesh May 12 '20 at 04:55
  • @rupesh: Sorry, but I do not understand your question. You need an `IntentSender` to pass to `commit()`. Those lines show how to set up an `IntentSender`. The actual broadcast is sent by the OS after the app is installed (or not). – CommonsWare May 12 '20 at 11:46
  • @CommonsWare thanks for clarification. My question is i.e. I have downloaded the apk from server now it is the latest version and I want to update the same app. In this case app won't receive any broadcast. – rupesh May 12 '20 at 12:10
  • @rupesh: Sorry, but I have not tried that scenario. – CommonsWare May 12 '20 at 12:23
  • 4
    Where can I find an official documentation where they say ACTION_VIEW is deprecated for apks? – AndreaGrnd Aug 04 '20 at 15:12
  • @AndreaGrnd: I will be surprised if there is official documentation to that effect. – CommonsWare Aug 04 '20 at 16:13
  • Great answer thank you. Could you please let me know how to show ui while app is installing ? Here is the question. https://stackoverflow.com/questions/70846768/how-to-show-app-installing-ui-or-notification-while-app-is-being-installing really appreciate with your help. – Saugat Rai Jan 25 '22 at 10:10
  • 3
    Using `FileProvider.getUriForFile(` and ACTION_VIEW with `addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)` worked for me as long as I had `` on Android 10 – EpicPandaForce Apr 07 '22 at 17:23
  • Using ```val intent = Intent(Intent.ACTION_VIEW, apkUri).apply { setDataAndType(apkUri, "application/vnd.android.package-archive") flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }``` is still working on Android 11 and 12. – CodingBruceLee May 04 '22 at 00:57
  • and jsut use `Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData( Uri.parse(String.format("package:%s", requireContext().packageName)) )` and `startActivityForResult(unknownIntent, UNKNOWN_SOURCE_INTENT_REQUEST_CODE)`. it' works well. – CodingBruceLee May 19 '22 at 07:31
  • @EpicPandaForce [As long as you have `TargetVersion>=26`.](https://stackoverflow.com/a/49828751/433718) – OneWorld Jan 10 '23 at 10:35