Add multi-instance state to item infos
- Add legacy resource for supported multi-instance apps that
matches the current SystemUI resource of the same name, and will
be removed as apps migrate to the V manifest property to declare
multi-instance support
- Load the multi-instance state from PackageManager when the db is
first loaded or when packages are updated
- The multi-instance check is then used to determine if an app pair
can be saved (ie. whether the action can be shown)
Bug: 323112914
Test: atest NexusLauncherTests
Change-Id: I565b4bee4ab5f7040910306b1fd60a4fc3bf9a1c
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 869b995..a4ae1c8 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -52,6 +52,7 @@
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 @@
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 be98589..e3da389 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -98,6 +98,8 @@
@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 @@
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 b9a62e2..98bba08 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -132,6 +132,10 @@
@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 39c1243..bcd6ad2 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -301,6 +301,7 @@
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<LauncherActivityInfo> matches = context.getSystemService(LauncherApps.class)
.getActivityList(packageName, user);
if (matches.size() > 0) {
@@ -330,7 +331,7 @@
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 0875974..84130c7 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.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 @@
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 int restoreFlag;
public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState,
+ PackageManagerHelper pmHelper,
@Nullable LauncherRestoreEventLogger restoreEventLogger) {
super(cursor);
@@ -121,6 +124,7 @@
allUsers = userManagerState.allUsers;
mContext = app.getContext();
mIconCache = app.getIconCache();
+ mPmHelper = pmHelper;
mIDP = app.getInvariantDeviceProfile();
mRestoreEventLogger = restoreEventLogger;
@@ -368,7 +372,7 @@
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 876bed4..0d40a24 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -435,7 +435,8 @@
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 @@
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 8360b14..2264d35 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -26,6 +26,7 @@
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 @@
* 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 @@
/**
* 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 802faae..6275ed0 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -126,8 +126,8 @@
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 @@
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 @@
if (isNewApkAvailable) {
List<LauncherActivityInfo> 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 @@
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 cea4380..90e47d6 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -336,7 +336,8 @@
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 18aa6e7..a4281f8 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -90,12 +90,12 @@
*/
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 @@
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 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 @@
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 d4c25cb..6ac44ff 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -127,6 +127,11 @@
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.
*/
@@ -252,6 +257,24 @@
}
/**
+ * 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.
*/
public void setNonResizeable(boolean nonResizeable) {
@@ -301,4 +324,11 @@
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 b992a92..3ae643e 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.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 @@
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 3684f56..f7c4df4 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 @@
@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 @@
}
}
+ /**
+ * 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<LauncherActivityInfo> 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 @@
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);
+ }
}