2

Our app downloads an APK from our server, and runs this to upgrade itself. As mentioned in Android 10 - No Activity found to handle Intent and Xamarin Android 10 Install APK - No Activity found to handle Intent, this does not work as previously if the mobile device has been upgraded to Android 10, getting "No Activity found to handle Intent".

We've tried to rewrite this using PackageInstaller as in the example https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/content/InstallApkSessionApi.java, but now get this error instead:

signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: 'JNI DETECTED ERROR IN APPLICATION: JNI GetStaticMethodID called with pending exception java.lang.SecurityException: Files still open
  at java.lang.Exception android.os.Parcel.createException(int, java.lang.String) (Parcel.java:2071)
  at void android.os.Parcel.readException(int, java.lang.String) (Parcel.java:2039)
  at void android.os.Parcel.readException() (Parcel.java:1987)
  at void android.content.pm.IPackageInstallerSession$Stub$Proxy.commit(android.content.IntentSender, boolean) (IPackageInstallerSession.java:593)
  at void android.content.pm.PackageInstaller$Session.commit(android.content.IntentSender) (PackageInstaller.java:1072)
  at void com.mycompany.myApp.QtJavaCustomBridge.JIntentActionInstallApk(java.lang.String) (QtJavaCustomBridge.java:301)
  at void org.qtproject.qt5.android.QtNative.startQtApplication() (QtNative.java:-2)
  at void org.qtproject.qt5.android.QtNative$7.run() (QtNative.java:374)
  at void org.qtproject.qt5.android.QtThread$1.run() (QtThread.java:61)
  at void java.lang.Thread.run() (Thread.java:919)
Caused by: android.os.RemoteException: Remote stack trace:
    at com.android.server.pm.PackageInstallerSession.assertNoWriteFileTransfersOpenLocked(PackageInstallerSession.java:837)
    at com.android.server.pm.PackageInstallerSession.sealAndValidateLocked(PackageInstallerSession.java:1094)
    at com.android.server.pm.PackageInstallerSession.markAsCommitted(PackageInstallerSession.java:987)
    at com.android.server.pm.PackageInstallerSession.commit(PackageInstallerSession.java:849)
    at android.content.pm.IPackageInstallerSession$Stub.onTransact(IPackageInstallerSession.java:306)
(Throwable with no stack trace)

Here's the code we use:

public static final String TAG = "JAVA"; 
public static final String PACKAGE_INSTALLED_ACTION = "com.mycompany.myApp.SESSION_API_PACKAGE_INSTALLED";

public static void JIntentActionInstallApk(final String filename)
{
    PackageInstaller.Session session = null;
    try {
        Log.i(TAG, "JIntentActionInstallApk " + filename);
        PackageInstaller packageInstaller = MyAppActivity.getActivityInstance().getPackageManager().getPackageInstaller();
        Log.i(TAG, "JIntentActionInstallApk - got packageInstaller");
        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        Log.i(TAG, "JIntentActionInstallApk - set SessionParams");
        int sessionId = packageInstaller.createSession(params);
        session = packageInstaller.openSession(sessionId);
        Log.i(TAG, "JIntentActionInstallApk - session opened");

        // Create an install status receiver.
        Context context = MyAppActivity.getActivityInstance().getApplicationContext();
        addApkToInstallSession(context, filename, session);
        Log.i(TAG, "JIntentActionInstallApk - apk added to session");

        Intent intent = new Intent(context, MyAppActivity.class);
        intent.setAction(MyAppActivity.PACKAGE_INSTALLED_ACTION);
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
        IntentSender statusReceiver = pendingIntent.getIntentSender();
        // Commit the session (this will start the installation workflow).
        session.commit(statusReceiver);
        Log.i(TAG, "JIntentActionInstallApk - commited");
    } catch (IOException e) {
        throw new RuntimeException("Couldn't install package", e);
    } catch (RuntimeException e) {
        if (session != null) {
            session.abandon();
        }
        throw e;
    }
}

private static void addApkToInstallSession(Context context, String filename, PackageInstaller.Session session)
{
       Log.i(TAG, "addApkToInstallSession " + filename);
       // It's recommended to pass the file size to openWrite(). Otherwise installation may fail
       // if the disk is almost full.
       try {
            OutputStream packageInSession = session.openWrite("package", 0, -1);
            InputStream input;
            Uri uri = Uri.parse(filename);
            input = context.getContentResolver().openInputStream(uri);

                if(input != null) {
                   Log.i(TAG, "input.available: " + input.available());
                   byte[] buffer = new byte[16384];
                   int n;
                   while ((n = input.read(buffer)) >= 0) {
                       packageInSession.write(buffer, 0, n);
                   }
                }
                else {
                    Log.i(TAG, "addApkToInstallSession failed");
                    throw new IOException ("addApkToInstallSession");
                }

       }
       catch (Exception e) {
           Log.i(TAG, "addApkToInstallSession failed2 " + e.toString());
       }
}

...

  @Override
    protected void onNewIntent(Intent intent) {
        Bundle extras = intent.getExtras();
        if (PACKAGE_INSTALLED_ACTION.equals(intent.getAction())) {
            int status = extras.getInt(PackageInstaller.EXTRA_STATUS);
            String message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE);
            switch (status) {
                case PackageInstaller.STATUS_PENDING_USER_ACTION:
                    // This test app isn't privileged, so the user has to confirm the install.
                    Intent confirmIntent = (Intent) extras.get(Intent.EXTRA_INTENT);
                    startActivity(confirmIntent);
                    break;
                case PackageInstaller.STATUS_SUCCESS:
                    Toast.makeText(this, "Install succeeded!", Toast.LENGTH_SHORT).show();
                    break;
                case PackageInstaller.STATUS_FAILURE:
                case PackageInstaller.STATUS_FAILURE_ABORTED:
                case PackageInstaller.STATUS_FAILURE_BLOCKED:
                case PackageInstaller.STATUS_FAILURE_CONFLICT:
                case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
                case PackageInstaller.STATUS_FAILURE_INVALID:
                case PackageInstaller.STATUS_FAILURE_STORAGE:
                    Toast.makeText(this, "Install failed! " + status + ", " + message,
                            Toast.LENGTH_SHORT).show();
                    break;
                default:
                    Toast.makeText(this, "Unrecognized status received from installer: " + status,
                            Toast.LENGTH_SHORT).show();
            }
        }
    }

Target SDK is set to API 23 to be able to support old devices some customers have. We're using Qt as the app framework, but Java for native Android functions like this.

Some thoughts on this:
* In Xamarin Android 10 Install APK - No Activity found to handle Intent, they mention that they need to do extra garbage collection in Xamarin. Maybe it's because of the same issue we have here? If so, how could we get passed this in Java directly?
* We're trying to upgrade the same app using a downloaded apk. Will this work at all using package installer? If not, do we then need to use a second app to upgrade the original?
* We also have a service running in the app as well for notifications, could this be causing the "Files still open" issue?

TomasL
  • 96
  • 1
  • 2
  • 10

1 Answers1

4

I was able to solve this by closing the InputStream and OutputStream. In addition, I had to check for SDK versions prior to 21, as we have minimum API 16 and PackageInstaller was added in API 21.

public static void JIntentActionInstallApk(final String filename)
    {
        PackageInstaller.Session session = null;
        try {
            Log.i(TAG, "JIntentActionInstallApk " + filename);

            if(Build.VERSION.SDK_INT < 21) {
                //as PackageInstaller was added in API 21, let's use the old way of doing it prior to 21
                Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
                Uri apkUri = Uri.parse(filename);
                Context context = MyAppActivity.getQtActivityInstance().getApplicationContext();
                ApplicationInfo appInfo = context.getApplicationInfo();
                intent.setData(apkUri);
                intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
                intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
                intent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,
                     appInfo.packageName);
                MyAppActivity.getQtActivityInstance().startActivity(intent);
            }
            else  {
                // API level 21 or higher, we need to use PackageInstaller
                PackageInstaller packageInstaller = MyAppActivity.getQtActivityInstance().getPackageManager().getPackageInstaller();
                Log.i(TAG, "JIntentActionInstallApk - got packageInstaller");
                PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                        PackageInstaller.SessionParams.MODE_FULL_INSTALL);
                Log.i(TAG, "JIntentActionInstallApk - set SessionParams");
                int sessionId = packageInstaller.createSession(params);
                session = packageInstaller.openSession(sessionId);
                Log.i(TAG, "JIntentActionInstallApk - session opened");

                // Create an install status receiver.
                Context context = MyAppActivity.getQtActivityInstance().getApplicationContext();
                addApkToInstallSession(context, filename, session);
                Log.i(TAG, "JIntentActionInstallApk - apk added to session");

                Intent intent = new Intent(context, MyAppActivity.class);
                intent.setAction(MyAppActivity.PACKAGE_INSTALLED_ACTION);
                PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
                IntentSender statusReceiver = pendingIntent.getIntentSender();
                // Commit the session (this will start the installation workflow).
                session.commit(statusReceiver);
                Log.i(TAG, "JIntentActionInstallApk - commited");
            }
        } catch (IOException e) {
            throw new RuntimeException("Couldn't install package", e);
        } catch (RuntimeException e) {
            if (session != null) {
                session.abandon();
            }
            throw e;
        }
    }

    private static void addApkToInstallSession(Context context, String filename, PackageInstaller.Session session)
    {
           Log.i(TAG, "addApkToInstallSession " + filename);
           // It's recommended to pass the file size to openWrite(). Otherwise installation may fail
           // if the disk is almost full.
           try {
                OutputStream packageInSession = session.openWrite("package", 0, -1);
                InputStream input;
                Uri uri = Uri.parse(filename);
                input = context.getContentResolver().openInputStream(uri);

                if(input != null) {
                   Log.i(TAG, "input.available: " + input.available());
                   byte[] buffer = new byte[16384];
                   int n;
                   while ((n = input.read(buffer)) >= 0) {
                       packageInSession.write(buffer, 0, n);
                   }
                }
                else {
                    Log.i(TAG, "addApkToInstallSession failed");
                    throw new IOException ("addApkToInstallSession");
                }
                packageInSession.close();  //need to close this stream
                input.close();             //need to close this stream
           }
           catch (Exception e) {
               Log.i(TAG, "addApkToInstallSession failed2 " + e.toString());
           }
   }

TomasL
  • 96
  • 1
  • 2
  • 10
  • What is filename name passed to addApkToInstallSession ? I get an error java.io.FileNotFoundException: No content provider: sample.com.sampleandroid.apk. I am passing file name as sample.com.sampleandroid.apk which needs to be installed – manjunath kallannavar Nov 04 '19 at 17:26
  • @manjunathkallannavar: In my case, the filename is the full path to the actual file, like this: file:///sdcard/temp/myapkfilename.apk – TomasL Nov 05 '19 at 22:15
  • Thank you I was able to resolve apk path. Now my onNewIntent method is not being called after session.commit(statusReceiver). When this method will be called ? – manjunath kallannavar Nov 14 '19 at 12:14
  • @manjunathkallannavar: If I remember correctly, onNewIntent is only called when you get back to the app. E.g. when you've started the app, click home, and then start the app again; now onNewIntent gets called if the app was already running. – TomasL Nov 27 '19 at 08:26
  • Thanks for your quick response. We use Cordova app and in native we perform app installation code . I have class that extends Activity and use below code context = this.classname ; I am not able to access getPackageManager().getPackageInstaller(); API on context with below code context.getPackageManager().getPackageInstaller(); I get null pointer exception . getPackageManager() coming as null . Do you have any idea ? Thank u – manjunath kallannavar Dec 04 '19 at 12:05
  • I don't know what this code does, but it just reopens the same activity for me.... – Sir NIkolay Cesar The First Apr 30 '20 at 08:10