javaandroidapkandroid-install-apkandroid-10.0

Installing apk that updates the same app fails on Android 10; java.lang.SecurityException: Files still open


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?


Solution

  • 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());
               }
       }