Android: Multiple instances of Launcher activity with PackageManager's queryIntentActivities

   Android's customization lets users download and install custom launchers starting from AOSP's Launcher2 or Launcher3 and even Google Now. One of the tasks for Launcher developers is to figure out the installed applications and create launch intents as and when users need them. Android's Package Manager helps in querying installed applications and one such API is queryIntentActivities.

    Here is a utility that app developers can use to find the Launch intent for a particular package.

    private Intent launchApp( String packageName )
    {
        Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
        intentToResolve.addCategory(Intent.CATEGORY_INFO);
        intentToResolve.setPackage(packageName);
        List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0);

        // Otherwise, try to find a main launcher activity.
        if (ris == null || ris.size() <= 0) {
            // reuse the intent instance
            intentToResolve.removeCategory(Intent.CATEGORY_INFO);
            intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
            intentToResolve.setPackage(packageName);
            ris = queryIntentActivities(intentToResolve, 0);
        }

        if (ris == null || ris.size() <= 0) {
            return null;
        }

        Intent intent = new Intent(intentToResolve);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setClassName(ris.get(0).activityInfo.packageName,
                ris.get(0).activityInfo.name);

        return intent;
    }

    The above function seems to be fine except that in certain cases it tends to launch another instance of the Launcher activity instead of resuming an existing instance/task. This is the case for launcher activities that don't request singleInstance or singleTask. So what is the scenario and why does Android's activity manager launch multiple instances instead of resuming the existing one (backgrounded by the user)?

   The use case is when the application is launched via Google Now launcher's App tray grid view, backgrounded by the user and when another 3rd party application tries to launch/resume the same launcher activity with the above utility function.

    The reason as to why Activity manager launches another instance is due to the difference in the original launch intent from Google Now and the one returned by queryIntentActivities. The original intent doesn't have package name where as the one returned from queryIntentActivities does have one. This difference causes the following check in ActivityStackSupervisor to go through and eventually leads to a new instance. The intent comparison fails because one of them (queryIntentActivities) has a package name.

    else if (!r.intent.filterEquals(intentActivity.task.intent)) {
             // In this case we are launching the root activity
             // of the task, but with a different intent.  We
             // should start a new instance on top.
             addingToTask = true;
             sourceRecord = intentActivity;
    }

   A quick fix is to reset the package name in the intent returned from PackageManager via intent.setPackageName(null) and this ensures that the existing instance is indeed resumed. The actual fix is needed in all launcher applications to set the package name before starting the first instance of the activity. One such fix is for AOSP's Launcher 2 at link.


 


No comments: