Fix app pair launches with certain apps
This CL changes the way app pairs are saved. Previously, we saved the app pair using WorkspaceItemInfos directly from Recents, which caused certain apps to fail on launch because they weren't being launched with the correct Intent.
Now, we look up a proper launchable WorkspaceItemInfo from the AllAppsStore and save that to the app pair, which should fix the launch issues.
Bug: 323112914
Bug: 323110399
Test: Can launch Slides, Play Store, and Calendar when part of an app pair.
Flag: ACONFIG com.android.wm.shell.enable_app_pairs TEAMFOOD
Change-Id: I475afa1a237c50ffb0ffaf85af43912a803b8011
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index b846323..ce8df9b 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -20,6 +20,7 @@
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
+import static com.android.launcher3.model.data.AppInfo.PACKAGE_KEY_COMPARATOR;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -30,6 +31,7 @@
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.LauncherApps;
import android.util.Log;
import android.util.Pair;
@@ -42,10 +44,12 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -93,14 +97,38 @@
}
/**
- * Creates a new app pair ItemInfo and adds it to the workspace
+ * Creates a new app pair ItemInfo and adds it to the workspace.
+ * <br>
+ * We create WorkspaceItemInfos to save onto the app pair in the following way:
+ * <br> 1. We verify that the ComponentKey from our Recents tile corresponds to a real
+ * launchable app in the app store.
+ * <br> 2. If it doesn't, we search for the underlying launchable app via package name, and use
+ * that instead.
+ * <br> 3. If that fails, we re-use the existing WorkspaceItemInfo by cloning it and replacing
+ * its intent with one from PackageManager.
+ * <br> 4. If everything fails, we just use the WorkspaceItemInfo as is, with its existing
+ * intent. This is not preferred, but will still work in most cases (notably it will not work
+ * well on trampoline apps).
*/
public void saveAppPair(GroupedTaskView gtv) {
TaskView.TaskIdAttributeContainer[] attributes = gtv.getTaskIdAttributeContainers();
- WorkspaceItemInfo app1 = attributes[0].getItemInfo().clone();
- WorkspaceItemInfo app2 = attributes[1].getItemInfo().clone();
- app1.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
- app2.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ WorkspaceItemInfo recentsInfo1 = attributes[0].getItemInfo();
+ WorkspaceItemInfo recentsInfo2 = attributes[0].getItemInfo();
+ WorkspaceItemInfo app1 = lookupLaunchableItem(recentsInfo1.getComponentKey());
+ WorkspaceItemInfo app2 = lookupLaunchableItem(recentsInfo2.getComponentKey());
+
+ // If app lookup fails, use the WorkspaceItemInfo that we have, but try to override default
+ // intent with one from PackageManager.
+ if (app1 == null) {
+ Log.w(TAG, "Creating an app pair, but app lookup for " + recentsInfo1.title
+ + " failed. Falling back to the WorkspaceItemInfo from Recents.");
+ app1 = convertRecentsItemToAppItem(recentsInfo1);
+ }
+ if (app2 == null) {
+ Log.w(TAG, "Creating an app pair, but app lookup for " + recentsInfo2.title
+ + " failed. Falling back to the WorkspaceItemInfo from Recents.");
+ app2 = convertRecentsItemToAppItem(recentsInfo2);
+ }
@PersistentSnapPosition int snapPosition = gtv.getSnapPosition();
if (!isPersistentSnapPosition(snapPosition)) {
@@ -189,6 +217,52 @@
}
/**
+ * Creates a new launchable WorkspaceItemInfo of itemType=ITEM_TYPE_APPLICATION by looking the
+ * ComponentKey up in the AllAppsStore. If no app is found, attempts a lookup by package
+ * instead. If that lookup fails, returns null.
+ */
+ @Nullable
+ private WorkspaceItemInfo lookupLaunchableItem(@Nullable ComponentKey key) {
+ if (key == null) {
+ return null;
+ }
+
+ AllAppsStore appsStore = Launcher.getLauncher(mContext).getAppsView().getAppsStore();
+
+ // Lookup by ComponentKey
+ AppInfo appInfo = appsStore.getApp(key);
+ if (appInfo == null) {
+ // Lookup by package
+ appInfo = appsStore.getApp(key, PACKAGE_KEY_COMPARATOR);
+ }
+
+ return appInfo != null ? appInfo.makeWorkspaceItem(mContext) : null;
+ }
+
+ /**
+ * Converts a WorkspaceItemInfo of itemType=ITEM_TYPE_TASK (from a Recents task) to a new
+ * WorkspaceItemInfo of itemType=ITEM_TYPE_APPLICATION.
+ */
+ private WorkspaceItemInfo convertRecentsItemToAppItem(WorkspaceItemInfo recentsItem) {
+ if (recentsItem.itemType != LauncherSettings.Favorites.ITEM_TYPE_TASK) {
+ Log.w(TAG, "Expected ItemInfo of type ITEM_TYPE_TASK, but received "
+ + recentsItem.itemType);
+ }
+
+ WorkspaceItemInfo launchableItem = recentsItem.clone();
+ PackageManager p = mContext.getPackageManager();
+ Intent launchIntent = p.getLaunchIntentForPackage(recentsItem.getTargetPackage());
+ Log.w(TAG, "Initial intent from Recents: " + launchableItem.intent + "\n"
+ + "Intent from PackageManager: " + launchIntent);
+ if (launchIntent != null) {
+ // If lookup from PackageManager fails, just use the existing intent
+ launchableItem.intent = launchIntent;
+ }
+ launchableItem.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ return launchableItem;
+ }
+
+ /**
* Handles the complicated logic for how to animate an app pair entrance when already inside an
* app or app pair.
*
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 009a2aa6..9623709 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -40,6 +40,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -138,12 +139,22 @@
/**
* Returns {@link AppInfo} if any apps matches with provided {@link ComponentKey}, otherwise
* null.
+ *
+ * Uses {@link AppInfo#COMPONENT_KEY_COMPARATOR} as a default comparator.
*/
@Nullable
public AppInfo getApp(ComponentKey key) {
+ return getApp(key, COMPONENT_KEY_COMPARATOR);
+ }
+
+ /**
+ * Generic version of {@link #getApp(ComponentKey)} that allows comparator to be specified.
+ */
+ @Nullable
+ public AppInfo getApp(ComponentKey key, Comparator<AppInfo> comparator) {
mTempInfo.componentName = key.componentName;
mTempInfo.user = key.user;
- int index = Arrays.binarySearch(mApps, mTempInfo, COMPONENT_KEY_COMPARATOR);
+ int index = Arrays.binarySearch(mApps, mTempInfo, comparator);
return index < 0 ? null : mApps[index];
}
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index b213fe3..210d720 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -52,6 +52,9 @@
return uc != 0 ? uc : a.componentName.compareTo(b.componentName);
};
+ public static final Comparator<AppInfo> PACKAGE_KEY_COMPARATOR = Comparator.comparingInt(
+ (AppInfo a) -> a.user.hashCode()).thenComparing(ItemInfo::getTargetPackage);
+
/**
* The intent used to start the application.
*/