diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 65a49bd7c9..8b5ed7cc4b 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -73,6 +73,7 @@ import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.ApiWrapper; import com.android.launcher3.util.Executors; import com.android.launcher3.util.IntSparseArrayMap; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PersistedItemArray; import com.android.quickstep.logging.SettingsChangeLogger; import com.android.quickstep.logging.StatsLogCompatManager; @@ -150,7 +151,8 @@ public class QuickstepModelDelegate extends ModelDelegate { // TODO: Implement caching and preloading WorkspaceItemFactory factory = - new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, numColumns, state.containerId); + new WorkspaceItemFactory(mApp, ums, mPmHelper, pinnedShortcuts, numColumns, + state.containerId); FixedContainerItems fci = new FixedContainerItems(state.containerId, state.storage.read(mApp.getContext(), factory, ums.allUsers::get)); if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) { @@ -530,6 +532,7 @@ public class QuickstepModelDelegate extends ModelDelegate { private final LauncherAppState mAppState; private final UserManagerState mUMS; + private final PackageManagerHelper mPmHelper; private final Map mPinnedShortcuts; private final int mMaxCount; private final int mContainer; @@ -537,9 +540,11 @@ public class QuickstepModelDelegate extends ModelDelegate { private int mReadCount = 0; protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, - Map pinnedShortcuts, int maxCount, int container) { + PackageManagerHelper pmHelper, Map pinnedShortcuts, + int maxCount, int container) { mAppState = appState; mUMS = ums; + mPmHelper = pmHelper; mPinnedShortcuts = pinnedShortcuts; mMaxCount = maxCount; mContainer = container; @@ -563,6 +568,7 @@ public class QuickstepModelDelegate extends ModelDelegate { lai, UserCache.INSTANCE.get(mAppState.getContext()).getUserInfo(user), ApiWrapper.INSTANCE.get(mAppState.getContext()), + mPmHelper, mUMS.isUserQuiet(user)); info.container = mContainer; mAppState.getIconCache().getTitleAndIcon(info, lai, false); diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java index f052a9d580..a53d91fd7d 100644 --- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java +++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java @@ -335,24 +335,15 @@ public interface TaskShortcutFactory { recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView)); boolean shouldShowActionsButtonInstead = isLargeTileFocusedTask && isInExpectedScrollPosition; - boolean hasUnpinnableApp = taskView.getTaskContainers().stream() - .anyMatch(att -> att != null && att.getItemInfo() != null - && ((att.getItemInfo().runtimeStatusFlags - & ItemInfoWithIcon.FLAG_NOT_PINNABLE) != 0)); // No "save app pair" menu item if: - // - app pairs feature is not enabled // - we are in 3p launcher - // - the task in question is a single task - // - at least one app in app pair is unpinnable // - the Overview Actions Button should be visible - // - the task is not a GroupedTaskView - if (!FeatureFlags.enableAppPairs() - || !recentsView.supportsAppPairs() - || !taskView.containsMultipleTasks() - || hasUnpinnableApp + // - the task view is not a valid save-able split pair + if (!recentsView.supportsAppPairs() || shouldShowActionsButtonInstead - || !(taskView instanceof GroupedTaskView)) { + || !recentsView.getSplitSelectController().getAppPairsController() + .canSaveAppPair(taskView)) { return null; } diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java index 770a4525cf..e4e2eb2769 100644 --- a/quickstep/src/com/android/quickstep/util/AppPairsController.java +++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java @@ -22,6 +22,7 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR; 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.model.data.ItemInfoWithIcon.FLAG_SUPPORTS_MULTI_INSTANCE; 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; @@ -32,34 +33,38 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.isPersisten import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; -import android.content.pm.PackageManager; import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.jank.Cuj; import com.android.launcher3.LauncherAppState; 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.config.FeatureFlags; 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.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; import com.android.launcher3.views.ActivityContext; import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TaskUtils; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.TaskView; @@ -100,6 +105,55 @@ public class AppPairsController { mContext = null; } + /** + * Returns whether the specified GroupedTaskView can be saved as an app pair. + */ + public boolean canSaveAppPair(TaskView taskView) { + if (mContext == null) { + // Can ignore as the activity is already destroyed + return false; + } + + // Disallow saving app pairs if: + // - app pairs feature is not enabled + // - the task in question is a single task + // - at least one app in app pair is unpinnable + // - the task is not a GroupedTaskView + // - both tasks in the GroupedTaskView are from the same app and the app does not + // support multi-instance + boolean hasUnpinnableApp = taskView.getTaskContainers().stream() + .anyMatch(att -> att != null && att.getItemInfo() != null + && ((att.getItemInfo().runtimeStatusFlags + & ItemInfoWithIcon.FLAG_NOT_PINNABLE) != 0)); + if (!FeatureFlags.enableAppPairs() + || !taskView.containsMultipleTasks() + || hasUnpinnableApp + || !(taskView instanceof GroupedTaskView)) { + return false; + } + + GroupedTaskView gtv = (GroupedTaskView) taskView; + List containers = gtv.getTaskContainers(); + ComponentKey taskKey1 = TaskUtils.getLaunchComponentKeyForTask( + containers.get(0).getTask().key); + ComponentKey taskKey2 = TaskUtils.getLaunchComponentKeyForTask( + containers.get(1).getTask().key); + AppInfo app1 = resolveAppInfoByComponent(taskKey1); + AppInfo app2 = resolveAppInfoByComponent(taskKey2); + + if (app1 == null || app2 == null) { + // Disallow saving app pairs for apps that don't have a front-door in Launcher + return false; + } + + if (PackageManagerHelper.isSameAppForMultiInstance(app1, app2)) { + if (!app1.supportsMultiInstance() || !app2.supportsMultiInstance()) { + return false; + } + } + return true; + } + /** * Creates a new app pair ItemInfo and adds it to the workspace. *
@@ -119,31 +173,23 @@ public class AppPairsController { List containers = gtv.getTaskContainers(); WorkspaceItemInfo recentsInfo1 = containers.get(0).getItemInfo(); WorkspaceItemInfo recentsInfo2 = containers.get(1).getItemInfo(); - WorkspaceItemInfo app1 = lookupLaunchableItem(recentsInfo1.getComponentKey()); - WorkspaceItemInfo app2 = lookupLaunchableItem(recentsInfo2.getComponentKey()); + WorkspaceItemInfo app1 = resolveAppPairWorkspaceInfo(recentsInfo1); + WorkspaceItemInfo app2 = resolveAppPairWorkspaceInfo(recentsInfo2); - // 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 (app1 == null || app2 == null) { + // This shouldn't happen if canSaveAppPair() is called above, but log an error and do + // not create the app pair if the workspace items can't be resolved + Log.w(TAG, "Failed to save app pair due to invalid apps (" + + "app1=" + recentsInfo1.getComponentKey().componentName + + " app2=" + recentsInfo2.getComponentKey().componentName + ")"); + return; } - 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); - } - - // WorkspaceItemProcessor won't process these new ItemInfos until the next launcher restart, - // so update some flags now. - updateWorkspaceItemFlags(app1); - updateWorkspaceItemFlags(app2); @PersistentSnapPosition int snapPosition = gtv.getSnapPosition(); if (!isPersistentSnapPosition(snapPosition)) { - // if we received an illegal snap position, log an error and do not create the app pair. - Log.wtf(TAG, "tried to save an app pair with illegal snapPosition " + snapPosition); + // If we received an illegal snap position, log an error and do not create the app pair + Log.wtf(TAG, "Tried to save an app pair with illegal snapPosition " + + snapPosition); return; } @@ -228,68 +274,39 @@ public class AppPairsController { ); } + /** + * Returns an AppInfo associated with the app for the given ComponentKey, or null if no such + * package exists in the AllAppsStore. + */ + @Nullable + private AppInfo resolveAppInfoByComponent(@NonNull ComponentKey key) { + AllAppsStore appsStore = ActivityContext.lookupContext(mContext) + .getAppsView().getAppsStore(); + + // First look up the app info in order of: + // - The exact activity for the recent task + // - The first(?) loaded activity from the package + AppInfo appInfo = appsStore.getApp(key); + if (appInfo == null) { + appInfo = appsStore.getApp(key, PACKAGE_KEY_COMPARATOR); + } + return appInfo; + } + /** * 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) { + private WorkspaceItemInfo resolveAppPairWorkspaceInfo( + @NonNull WorkspaceItemInfo recentTaskInfo) { + // ComponentKey should never be null (see TaskView#getItemInfo) + AppInfo appInfo = resolveAppInfoByComponent(recentTaskInfo.getComponentKey()); + if (appInfo == null) { return null; } - - AllAppsStore appsStore = ActivityContext.lookupContext(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; - } - - /** - * Updates flags for newly created WorkspaceItemInfos. - */ - private void updateWorkspaceItemFlags(WorkspaceItemInfo wii) { - PackageManager pm = mContext.getPackageManager(); - ActivityInfo ai = null; - try { - ai = pm.getActivityInfo(wii.getTargetComponent(), 0); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "PackageManager lookup failed."); - } - - if (ai != null) { - wii.setNonResizeable(ai.resizeMode == ActivityInfo.RESIZE_MODE_UNRESIZEABLE); - } - } - - /** - * 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; + return appInfo.makeWorkspaceItem(mContext); } /** diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java index 8a917d68bf..ba60141aab 100644 --- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java +++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java @@ -38,6 +38,7 @@ import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.NavigationMode; import com.android.quickstep.TaskOverlayFactory.OverlayUICallbacks; +import com.android.quickstep.util.AppPairsController; import com.android.quickstep.util.LayoutUtils; import java.lang.annotation.Retention; @@ -139,6 +140,7 @@ public class OverviewActionsView extends FrameLayo protected DeviceProfile mDp; private final Rect mTaskSize = new Rect(); private boolean mIsGroupedTask = false; + private boolean mCanSaveAppPair = false; public OverviewActionsView(Context context) { this(context, null); @@ -245,9 +247,12 @@ public class OverviewActionsView extends FrameLayo * Updates a batch of flags to hide and show actions buttons when a grouped task (split screen) * is focused. * @param isGroupedTask True if the focused task is a grouped task. + * @param canSaveAppPair True if the focused task is a grouped task and can be saved as an app + * pair. */ - public void updateForGroupedTask(boolean isGroupedTask) { + public void updateForGroupedTask(boolean isGroupedTask, boolean canSaveAppPair) { mIsGroupedTask = isGroupedTask; + mCanSaveAppPair = canSaveAppPair; updateActionButtonsVisibility(); } @@ -264,7 +269,7 @@ public class OverviewActionsView extends FrameLayo private void updateActionButtonsVisibility() { assert mDp != null; boolean showSingleTaskActions = !mIsGroupedTask; - boolean showGroupActions = mIsGroupedTask && mDp.isTablet; + boolean showGroupActions = mIsGroupedTask && mDp.isTablet && mCanSaveAppPair; getActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showSingleTaskActions ? 1 : 0); getGroupActionsAlphas().get(INDEX_GROUPED_ALPHA).setValue(showGroupActions ? 1 : 0); } diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index b5ce7f7075..9c99dfc698 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -4011,7 +4011,9 @@ public abstract class RecentsView com.android.settings + + + + diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 869b9956c1..a4ae1c8050 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -52,6 +52,7 @@ import com.android.launcher3.pm.InstallSessionTracker; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.RunnableList; import com.android.launcher3.util.SafeCloseable; @@ -181,7 +182,7 @@ public class LauncherAppState implements SafeCloseable { mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName, mIconProvider); mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext), - iconCacheFileName != null); + PackageManagerHelper.INSTANCE.get(context), iconCacheFileName != null); mOnTerminateCallback.add(mIconCache::close); mOnTerminateCallback.add(mModel::destroy); } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index be98589467..e3da38937d 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -98,6 +98,8 @@ public class LauncherModel implements InstallSessionTracker.Callback { @NonNull private final LauncherAppState mApp; @NonNull + private final PackageManagerHelper mPmHelper; + @NonNull private final ModelDbController mModelDbController; @NonNull private final Object mLock = new Object(); @@ -152,12 +154,13 @@ public class LauncherModel implements InstallSessionTracker.Callback { LauncherModel(@NonNull final Context context, @NonNull final LauncherAppState app, @NonNull final IconCache iconCache, @NonNull final AppFilter appFilter, - final boolean isPrimaryInstance) { + @NonNull final PackageManagerHelper pmHelper, final boolean isPrimaryInstance) { mApp = app; + mPmHelper = pmHelper; mModelDbController = new ModelDbController(context); mBgAllAppsList = new AllAppsList(iconCache, appFilter); - mModelDelegate = ModelDelegate.newInstance(context, app, mBgAllAppsList, mBgDataModel, - isPrimaryInstance); + mModelDelegate = ModelDelegate.newInstance(context, app, mPmHelper, mBgAllAppsList, + mBgDataModel, isPrimaryInstance); } @NonNull diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index b9a62e25b2..98bba08942 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -132,6 +132,10 @@ public final class Utilities { @ChecksSdkIntAtLeast(api = VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "U") public static final boolean ATLEAST_U = Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE; + @ChecksSdkIntAtLeast(api = VERSION_CODES.VANILLA_ICE_CREAM, codename = "V") + public static final boolean ATLEAST_V = Build.VERSION.SDK_INT + >= VERSION_CODES.VANILLA_ICE_CREAM; + /** * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}. */ diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java index 39c1243bd1..bcd6ad2453 100644 --- a/src/com/android/launcher3/model/AllAppsList.java +++ b/src/com/android/launcher3/model/AllAppsList.java @@ -301,6 +301,7 @@ public class AllAppsList { Context context, String packageName, UserHandle user) { final ApiWrapper apiWrapper = ApiWrapper.INSTANCE.get(context); final UserCache userCache = UserCache.getInstance(context); + final PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context); final List matches = context.getSystemService(LauncherApps.class) .getActivityList(packageName, user); if (matches.size() > 0) { @@ -330,7 +331,7 @@ public class AllAppsList { applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title); applicationInfo.intent = launchIntent; AppInfo.updateRuntimeFlagsForActivityTarget(applicationInfo, info, - userCache.getUserInfo(user), apiWrapper); + userCache.getUserInfo(user), apiWrapper, pmHelper); mDataChanged = true; } } diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 0875974a21..84130c796f 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -57,6 +57,7 @@ import com.android.launcher3.util.ContentWriter; import com.android.launcher3.util.GridOccupancy; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSparseArrayMap; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.UserIconInfo; import java.net.URISyntaxException; @@ -73,6 +74,7 @@ public class LoaderCursor extends CursorWrapper { private final LauncherAppState mApp; private final Context mContext; + private final PackageManagerHelper mPmHelper; private final IconCache mIconCache; private final InvariantDeviceProfile mIDP; private final @Nullable LauncherRestoreEventLogger mRestoreEventLogger; @@ -114,6 +116,7 @@ public class LoaderCursor extends CursorWrapper { public int restoreFlag; public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState, + PackageManagerHelper pmHelper, @Nullable LauncherRestoreEventLogger restoreEventLogger) { super(cursor); @@ -121,6 +124,7 @@ public class LoaderCursor extends CursorWrapper { allUsers = userManagerState.allUsers; mContext = app.getContext(); mIconCache = app.getIconCache(); + mPmHelper = pmHelper; mIDP = app.getInvariantDeviceProfile(); mRestoreEventLogger = restoreEventLogger; @@ -368,7 +372,7 @@ public class LoaderCursor extends CursorWrapper { if (mActivityInfo != null) { AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo, - ApiWrapper.INSTANCE.get(mContext)); + ApiWrapper.INSTANCE.get(mContext), mPmHelper); } // from the db diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 876bed421f..0d40a2432e 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -435,7 +435,8 @@ public class LoaderTask implements Runnable { mShortcutKeyToPinnedShortcuts = new HashMap<>(); final LoaderCursor c = new LoaderCursor( dbController.query(TABLE_NAME, null, selection, null, null), - mApp, mUserManagerState, mIsRestoreFromBackup ? restoreEventLogger : null); + mApp, mUserManagerState, mPmHelper, + mIsRestoreFromBackup ? restoreEventLogger : null); final Bundle extras = c.getExtras(); mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME); try { @@ -697,7 +698,7 @@ public class LoaderTask implements Runnable { for (int i = 0; i < apps.size(); i++) { LauncherActivityInfo app = apps.get(i); AppInfo appInfo = new AppInfo(app, mUserCache.getUserInfo(user), - ApiWrapper.INSTANCE.get(mApp.getContext()), quietMode); + ApiWrapper.INSTANCE.get(mApp.getContext()), mPmHelper, quietMode); if (Flags.enableSupportForArchiving() && app.getApplicationInfo().isArchived) { // For archived apps, include progress info in case there is a pending // install session post restart of device. diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java index 8360b14fa4..2264d35df1 100644 --- a/src/com/android/launcher3/model/ModelDelegate.java +++ b/src/com/android/launcher3/model/ModelDelegate.java @@ -26,6 +26,7 @@ import androidx.annotation.WorkerThread; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.shortcuts.ShortcutKey; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.ResourceBasedOverride; import java.io.FileDescriptor; @@ -41,15 +42,16 @@ public class ModelDelegate implements ResourceBasedOverride { * Creates and initializes a new instance of the delegate */ public static ModelDelegate newInstance( - Context context, LauncherAppState app, AllAppsList appsList, BgDataModel dataModel, - boolean isPrimaryInstance) { + Context context, LauncherAppState app, PackageManagerHelper pmHelper, + AllAppsList appsList, BgDataModel dataModel, boolean isPrimaryInstance) { ModelDelegate delegate = Overrides.getObject( ModelDelegate.class, context, R.string.model_delegate_class); - delegate.init(app, appsList, dataModel, isPrimaryInstance); + delegate.init(app, pmHelper, appsList, dataModel, isPrimaryInstance); return delegate; } protected final Context mContext; + protected PackageManagerHelper mPmHelper; protected LauncherAppState mApp; protected AllAppsList mAppsList; protected BgDataModel mDataModel; @@ -62,9 +64,10 @@ public class ModelDelegate implements ResourceBasedOverride { /** * Initializes the object with the given params. */ - private void init(LauncherAppState app, AllAppsList appsList, + private void init(LauncherAppState app, PackageManagerHelper pmHelper, AllAppsList appsList, BgDataModel dataModel, boolean isPrimaryInstance) { this.mApp = app; + this.mPmHelper = pmHelper; this.mAppsList = appsList; this.mDataModel = dataModel; this.mIsPrimaryInstance = isPrimaryInstance; diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 802faae136..6275ed0630 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -126,8 +126,8 @@ public class PackageUpdatedTask implements ModelUpdateTask { if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) { appsList.removePackage(packages[i], mUser); } - activitiesLists.put( - packages[i], appsList.addPackage(context, packages[i], mUser)); + activitiesLists.put(packages[i], + appsList.addPackage(context, packages[i], mUser)); } flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE); break; @@ -138,8 +138,8 @@ public class PackageUpdatedTask implements ModelUpdateTask { for (int i = 0; i < N; i++) { if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]); iconCache.updateIconsForPkg(packages[i], mUser); - activitiesLists.put( - packages[i], appsList.updatePackage(context, packages[i], mUser)); + activitiesLists.put(packages[i], + appsList.updatePackage(context, packages[i], mUser)); } } // Since package was just updated, the target must be available now. @@ -269,6 +269,8 @@ public class PackageUpdatedTask implements ModelUpdateTask { if (isNewApkAvailable) { List activities = activitiesLists.get( packageName); + // TODO: See if we can migrate this to + // AppInfo#updateRuntimeFlagsForActivityTarget si.setProgressLevel( activities == null || activities.isEmpty() ? 100 @@ -399,7 +401,8 @@ public class PackageUpdatedTask implements ModelUpdateTask { return false; } // Try to find the best match activity. - Intent intent = new PackageManagerHelper(context).getAppLaunchIntent(packageName, mUser); + Intent intent = PackageManagerHelper.INSTANCE.get(context) + .getAppLaunchIntent(packageName, mUser); if (intent != null) { si.intent = intent; si.status = WorkspaceItemInfo.DEFAULT; diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt index cea4380e5e..90e47d66cc 100644 --- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt +++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt @@ -336,7 +336,8 @@ class WorkspaceItemProcessor( info, activityInfo, userCache.getUserInfo(c.user), - ApiWrapper.INSTANCE[app.context] + ApiWrapper.INSTANCE[app.context], + pmHelper ) } if ( diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java index 18aa6e75f7..a4281f8677 100644 --- a/src/com/android/launcher3/model/data/AppInfo.java +++ b/src/com/android/launcher3/model/data/AppInfo.java @@ -90,12 +90,12 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { */ public AppInfo(Context context, LauncherActivityInfo info, UserHandle user) { this(info, UserCache.INSTANCE.get(context).getUserInfo(user), - ApiWrapper.INSTANCE.get(context), + ApiWrapper.INSTANCE.get(context), PackageManagerHelper.INSTANCE.get(context), context.getSystemService(UserManager.class).isQuietModeEnabled(user)); } public AppInfo(LauncherActivityInfo info, UserIconInfo userIconInfo, - ApiWrapper apiWrapper, boolean quietModeEnabled) { + ApiWrapper apiWrapper, PackageManagerHelper pmHelper, boolean quietModeEnabled) { this.componentName = info.getComponentName(); this.container = CONTAINER_ALL_APPS; this.user = userIconInfo.user; @@ -105,7 +105,7 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { runtimeStatusFlags |= FLAG_DISABLED_QUIET_USER; } uid = info.getApplicationInfo().uid; - updateRuntimeFlagsForActivityTarget(this, info, userIconInfo, apiWrapper); + updateRuntimeFlagsForActivityTarget(this, info, userIconInfo, apiWrapper, pmHelper); } public AppInfo(AppInfo info) { @@ -184,7 +184,7 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { */ public static boolean updateRuntimeFlagsForActivityTarget( ItemInfoWithIcon info, LauncherActivityInfo lai, UserIconInfo userIconInfo, - ApiWrapper apiWrapper) { + ApiWrapper apiWrapper, PackageManagerHelper pmHelper) { final int oldProgressLevel = info.getProgressLevel(); final int oldRuntimeStatusFlags = info.runtimeStatusFlags; ApplicationInfo appInfo = lai.getApplicationInfo(); @@ -216,6 +216,8 @@ public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory { PackageManagerHelper.getLoadingProgress(lai), PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); info.setNonResizeable(apiWrapper.isNonResizeableActivity(lai)); + info.setSupportsMultiInstance( + pmHelper.supportsMultiInstance(lai.getComponentName())); return (oldProgressLevel != info.getProgressLevel()) || (oldRuntimeStatusFlags != info.runtimeStatusFlags); } diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java index d4c25cba8c..6ac44ffc93 100644 --- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java +++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java @@ -126,6 +126,11 @@ public abstract class ItemInfoWithIcon extends ItemInfo { */ public static final int FLAG_NOT_RESIZEABLE = 1 << 15; + /** + * Flag indicating whether the package related to the item & user supports multiple instances. + */ + public static final int FLAG_SUPPORTS_MULTI_INSTANCE = 1 << 16; + /** * Status associated with the system state of the underlying item. This is calculated every * time a new info is created and not persisted on the disk. @@ -251,6 +256,24 @@ public abstract class ItemInfoWithIcon extends ItemInfo { } } + /** + * Sets whether this app info supports multi-instance. + */ + protected void setSupportsMultiInstance(boolean supportsMultiInstance) { + if (supportsMultiInstance) { + runtimeStatusFlags |= FLAG_SUPPORTS_MULTI_INSTANCE; + } else { + runtimeStatusFlags &= ~FLAG_SUPPORTS_MULTI_INSTANCE; + } + } + + /** + * Returns whether this app info supports multi-instance. + */ + public boolean supportsMultiInstance() { + return (runtimeStatusFlags & FLAG_SUPPORTS_MULTI_INSTANCE) != 0; + } + /** * Sets whether this app info is non-resizeable. */ @@ -301,4 +324,11 @@ public abstract class ItemInfoWithIcon extends ItemInfo { drawable.setIsDisabled(isDisabled()); return drawable; } + + @Override + protected String dumpProperties() { + return super.dumpProperties() + + " supportsMultiInstance=" + supportsMultiInstance() + + " nonResizeable=" + isNonResizeable(); + } } diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java index b992a92ee4..3ae643e5b5 100644 --- a/src/com/android/launcher3/provider/LauncherDbUtils.java +++ b/src/com/android/launcher3/provider/LauncherDbUtils.java @@ -44,6 +44,7 @@ import com.android.launcher3.pm.UserCache; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; +import com.android.launcher3.util.PackageManagerHelper; /** * A set of utility methods for Launcher DB used for DB updates and migration. @@ -107,9 +108,11 @@ public class LauncherDbUtils { Cursor c = db.query( Favorites.TABLE_NAME, null, "itemType = 1", null, null, null, null); UserManagerState ums = new UserManagerState(); + PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context); ums.init(UserCache.INSTANCE.get(context), context.getSystemService(UserManager.class)); - LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, null); + LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper, + null); IntSet deletedShortcuts = new IntSet(); while (lc.moveToNext()) { diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java index 3684f569c3..f7c4df4527 100644 --- a/src/com/android/launcher3/util/PackageManagerHelper.java +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -17,6 +17,7 @@ package com.android.launcher3.util; import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; +import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI; import android.content.ActivityNotFoundException; import android.content.ComponentName; @@ -73,10 +74,14 @@ public class PackageManagerHelper implements SafeCloseable{ @NonNull private final LauncherApps mLauncherApps; + private final String[] mLegacyMultiInstanceSupportedApps; + public PackageManagerHelper(@NonNull final Context context) { mContext = context; mPm = context.getPackageManager(); mLauncherApps = Objects.requireNonNull(context.getSystemService(LauncherApps.class)); + mLegacyMultiInstanceSupportedApps = mContext.getResources().getStringArray( + R.array.config_appsSupportMultiInstancesSplit); } @Override @@ -159,11 +164,23 @@ public class PackageManagerHelper implements SafeCloseable{ } } + /** + * Returns the preferred launch activity intent for a given package. + */ @Nullable public Intent getAppLaunchIntent(@Nullable final String pkg, @NonNull final UserHandle user) { + LauncherActivityInfo info = getAppLaunchInfo(pkg, user); + return info != null ? AppInfo.makeLaunchIntent(info) : null; + } + + /** + * Returns the preferred launch activity for a given package. + */ + @Nullable + public LauncherActivityInfo getAppLaunchInfo(@Nullable final String pkg, + @NonNull final UserHandle user) { List activities = mLauncherApps.getActivityList(pkg, user); - return activities.isEmpty() ? null : - AppInfo.makeLaunchIntent(activities.get(0)); + return activities.isEmpty() ? null : activities.get(0); } /** @@ -285,4 +302,47 @@ public class PackageManagerHelper implements SafeCloseable{ return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 || ( Flags.enableSupportForArchiving() && info.isArchived); } + + /** + * Returns whether the given component or its application has the multi-instance property set. + */ + public boolean supportsMultiInstance(@NonNull ComponentName component) { + // Check the legacy hardcoded allowlist first + for (String pkg : mLegacyMultiInstanceSupportedApps) { + if (pkg.equals(component.getPackageName())) { + return true; + } + } + + // Check app multi-instance properties after V + if (!Utilities.ATLEAST_V) { + return false; + } + + try { + // Check if the component has the multi-instance property + return mPm.getProperty(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, component) + .getBoolean(); + } catch (PackageManager.NameNotFoundException e1) { + try { + // Check if the application has the multi-instance property + return mPm.getProperty(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, + component.getPackageName()) + .getBoolean(); + } catch (PackageManager.NameNotFoundException e2) { + // Fall through + } + } + return false; + } + + /** + * Returns whether two apps should be considered the same for multi-instance purposes, which + * requires additional checks to ensure they can be started as multiple instances. + */ + public static boolean isSameAppForMultiInstance(@NonNull ItemInfo app1, + @NonNull ItemInfo app2) { + return app1.getTargetPackage().equals(app2.getTargetPackage()) + && app1.user.equals(app2.user); + } } diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml index a9b75eaf41..8c5195e096 100644 --- a/tests/AndroidManifest-common.xml +++ b/tests/AndroidManifest-common.xml @@ -412,5 +412,9 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java index 56ac960c21..b4945d7129 100644 --- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java +++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java @@ -79,6 +79,7 @@ public class LoaderCursorTest { private LauncherModelHelper mModelHelper; private LauncherAppState mApp; + private PackageManagerHelper mPmHelper; private MatrixCursor mCursor; private InvariantDeviceProfile mIDP; @@ -92,6 +93,7 @@ public class LoaderCursorTest { mContext = mModelHelper.sandboxContext; mIDP = InvariantDeviceProfile.INSTANCE.get(mContext); mApp = LauncherAppState.getInstance(mContext); + mPmHelper = PackageManagerHelper.INSTANCE.get(mContext); mCursor = new MatrixCursor(new String[] { ICON, TITLE, _ID, CONTAINER, ITEM_TYPE, @@ -101,7 +103,7 @@ public class LoaderCursorTest { }); UserManagerState ums = new UserManagerState(); - mLoaderCursor = new LoaderCursor(mCursor, mApp, ums, null); + mLoaderCursor = new LoaderCursor(mCursor, mApp, ums, mPmHelper, null); ums.allUsers.put(0, Process.myUserHandle()); } diff --git a/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java index d1da5f4f2d..b5e797ed39 100644 --- a/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java +++ b/tests/src/com/android/launcher3/util/PackageManagerHelperTest.java @@ -34,6 +34,7 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Before; import org.junit.Rule; @@ -63,6 +64,8 @@ public final class PackageManagerHelperTest { mContext = mock(Context.class); mLauncherApps = mock(LauncherApps.class); when(mContext.getSystemService(eq(LauncherApps.class))).thenReturn(mLauncherApps); + when(mContext.getResources()).thenReturn( + InstrumentationRegistry.getInstrumentation().getTargetContext().getResources()); mPackageManagerHelper = new PackageManagerHelper(mContext); }