Search code examples
androidapkandroid-pendingintentandroid-package-managers

How to prevent pendingintent from recreating an activity if launchmode set to 'singleTop'?


I have some trouble using Packagemanager to install an update of my application. My application is designed for internal use at my firm, so we can not use Google Play to handle those updates. So after launching the application, it quickly checks the version and downloads the update (if any).

Here is what I tried in my activity ( the file is the downloaded and newer version of the same application)

    public void installNewApkVersion(File file){
        PackageInstaller.Session session = null;
        try{
            PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
            PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
            int sessionId = packageInstaller.createSession(params);
            session = packageInstaller.openSession(sessionId);

            addApkToInstallSession(session, file);
            // Create an install status receiver.
            Context context = LoginActivity.this;
            Intent intent = new Intent(context, LoginActivity.class);
            intent.setAction(ACTION_INSTALL_COMPLETE);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
            IntentSender statusReceiver = pendingIntent.getIntentSender();
            // Commit the session (this will start the installation workflow).
            session.commit(statusReceiver);
        } catch(IOException e){
            throw new RuntimeException("Couldn't install package", e);
        } catch (RuntimeException e){
            if(session != null){
                session.abandon();
            }
            throw e;
        }
    }

For this to work, I had to override onNewIntent like so:

  @Override
    protected void onNewIntent(Intent intent) {
        Bundle extras = intent.getExtras();

        boolean isloggedin = pref.getBoolean("Loggedin", false);



        if (ACTION_INSTALL_COMPLETE.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:
                    Log.w(TAG, "Install finished successfully");
                    break;
                case PackageInstaller.STATUS_FAILURE:
                    Toast.makeText(this, "Install failed!", Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Installation failed - status failure.");
                    break;
                case PackageInstaller.STATUS_FAILURE_ABORTED:
                    Toast.makeText(this, "Install failure aborted!", Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Installation failed. Aborted.");
                    break;
                case PackageInstaller.STATUS_FAILURE_BLOCKED:
                    Toast.makeText(this, "Install: failure blocked!", Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Installation failed. Blocked.");
                    break;
                case PackageInstaller.STATUS_FAILURE_CONFLICT:
                    Toast.makeText(this, "Install: failure conflicted!", Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Installation failed. Found conflicts.");
                    break;
                case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
                    Toast.makeText(this, "Install: incompatible!", Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Installation failed. Incompatible.");
                    break;
                case PackageInstaller.STATUS_FAILURE_INVALID:
                    Toast.makeText(this, "Install: invalid!", Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Installation failed. Invalid.");
                    break;
                case PackageInstaller.STATUS_FAILURE_STORAGE:
                    Toast.makeText(this, "Install failed: storage error! " + status + ", " + message,
                            Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Install failed. Storage error.");
                    break;
                default:
                    Toast.makeText(this, "Unrecognized status received from installer: " + status,
                            Toast.LENGTH_SHORT).show();
                    Log.w(TAG, "Dont know what happened");
            }
        }
    }

This Activity runs in singleTop mode in order to receive the result of the installation. It runs fine unless the user cancels it when prompted.

Because if they so, the activity is recreated again instead of passing the result back to the original in @onNewIntent resulting two instance of the same activity in the backstack.

I suspected this issue happens on android 7, when tested android 10, instead of recreating the activity the task routed to @onNewIntent (STATUS_FAILURE_ABORTED) as it should be.

How can I catch, if user aborts the installation?

The real purpose of this is to prevent the users from using my application if they have older version.

Or is there a way to somehow prevent this duplicate activities?


Solution

  • When the new instance of the Activity is created on Android 7, if the Intent contains the package installer status, you can do this in onCreate():

    Intent x = getIntent(); // This contains the package installer status
    x.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
    startActivity(x);
    finish();
    

    This will finish the new instance of the Activity and resume the previous instance of the Activity and pass the Intent in to onNewIntent() of the previous Activity.

    NOTE: The flag FLAG_ACTIVITY_PREVIOUS_IS_TOP tells Android that it should ignore the current topmost Activity when deciding what to do with the startActivity() call. When you set this flag you are telling Android: "This Activity is going to finish, so don't consider it when deciding if you need to create a new instance or use an existing instance"

    It is a hack, but should work :-)