Shortcut: Only "main" activities can have shortcuts.
- Don't publish shortcuts when their target activities are not main.
- Only scan manifest shortcuts for main activities.
- When an app is updated, remove shortcuts that no longer belong to
valid main activities.
- Also re-publish manifest shortcuts after 'clear data'
- Also listen to PACKAGE_CHANGED and disable/re-publish shortcuts
properly.
Bug 29355786
Bug 29582255
Bug 29601844
Change-Id: I6c701ce669cf30a227bc2af4aa01de467ef73e3a
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index a25ee3c..35370f0 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -709,7 +709,7 @@
@NonNull
@Deprecated
public Builder setId(@NonNull String id) {
- mId = Preconditions.checkStringNotEmpty(id, "id");
+ mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
return this;
}
@@ -721,14 +721,17 @@
*/
public Builder(Context context, String id) {
mContext = context;
- mId = Preconditions.checkStringNotEmpty(id, "id");
+ mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
}
/**
* Sets the target activity. A shortcut will be shown with this activity on the launcher.
*
- * <p>This is a mandatory field, unless it's passed to
- * {@link ShortcutManager#updateShortcuts(List)}.
+ * <p>Only "main" activities -- i.e. ones with an intent filter for
+ * {@link Intent#ACTION_MAIN} and {@link Intent#CATEGORY_LAUNCHER} can be target activities.
+ *
+ * <p>By default, the first main activity defined in the application manifest will be
+ * the target.
*
* <p>The package name of the target activity must match the package name of the shortcut
* publisher.
@@ -738,7 +741,7 @@
*/
@NonNull
public Builder setActivity(@NonNull ComponentName activity) {
- mActivity = Preconditions.checkNotNull(activity, "activity");
+ mActivity = Preconditions.checkNotNull(activity, "activity cannot be null");
return this;
}
@@ -785,7 +788,7 @@
@NonNull
public Builder setShortLabel(@NonNull CharSequence shortLabel) {
Preconditions.checkState(mTitleResId == 0, "shortLabelResId already set");
- mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel");
+ mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel cannot be empty");
return this;
}
@@ -810,7 +813,7 @@
@NonNull
public Builder setLongLabel(@NonNull CharSequence longLabel) {
Preconditions.checkState(mTextResId == 0, "longLabelResId already set");
- mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel");
+ mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel cannot be empty");
return this;
}
@@ -854,7 +857,8 @@
Preconditions.checkState(
mDisabledMessageResId == 0, "disabledMessageResId already set");
mDisabledMessage =
- Preconditions.checkStringNotEmpty(disabledMessage, "disabledMessage");
+ Preconditions.checkStringNotEmpty(disabledMessage,
+ "disabledMessage cannot be empty");
return this;
}
@@ -876,8 +880,8 @@
*/
@NonNull
public Builder setIntent(@NonNull Intent intent) {
- mIntent = Preconditions.checkNotNull(intent, "intent");
- Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set");
+ mIntent = Preconditions.checkNotNull(intent, "intent cannot be null");
+ Preconditions.checkNotNull(mIntent.getAction(), "intent's action must be set");
return this;
}
@@ -944,6 +948,11 @@
return mActivity;
}
+ /** @hide */
+ public void setActivity(ComponentName activity) {
+ mActivity = activity;
+ }
+
/**
* Icon.
*
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index d637586..67e4e93 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -54,8 +54,6 @@
* User information used by {@link ShortcutService}.
*
* All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
- *
- * TODO Max dynamic shortcuts cap should be per activity.
*/
class ShortcutPackage extends ShortcutPackageItem {
private static final String TAG = ShortcutService.TAG;
@@ -321,9 +319,27 @@
/**
* Remove a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
* is pinned, it'll remain as a pinned shortcut, and is still enabled.
+ *
+ * @return true if it's actually removed because it wasn't pinned, or false if it's still
+ * pinned.
*/
- public void deleteDynamicWithId(@NonNull String shortcutId) {
- deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false);
+ public boolean deleteDynamicWithId(@NonNull String shortcutId) {
+ final ShortcutInfo removed = deleteOrDisableWithId(
+ shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false);
+ return removed == null;
+ }
+
+ /**
+ * Disable a dynamic shortcut by ID. It'll be removed from the dynamic set, but if the shortcut
+ * is pinned, it'll remain as a pinned shortcut, but will be disabled.
+ *
+ * @return true if it's actually removed because it wasn't pinned, or false if it's still
+ * pinned.
+ */
+ private boolean disableDynamicWithId(@NonNull String shortcutId) {
+ final ShortcutInfo disabled = deleteOrDisableWithId(
+ shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false);
+ return disabled == null;
}
/**
@@ -599,14 +615,14 @@
*
* @return TRUE if any shortcuts have been changed.
*/
- public boolean handlePackageAddedOrUpdated(boolean isNewApp) {
+ public boolean handlePackageAddedOrUpdated(boolean isNewApp, boolean forceRescan) {
final PackageInfo pi = mShortcutUser.mService.getPackageInfo(
getPackageName(), getPackageUserId());
if (pi == null) {
return false; // Shouldn't happen.
}
- if (!isNewApp) {
+ if (!isNewApp && !forceRescan) {
// Make sure the version code or last update time has changed.
// Otherwise, nothing to do.
if (getPackageInfo().getVersionCode() >= pi.versionCode
@@ -649,12 +665,26 @@
boolean changed = false;
// For existing shortcuts, update timestamps if they have any resources.
+ // Also check if shortcuts' activities are still main activities. Otherwise, disable them.
if (!isNewApp) {
Resources publisherRes = null;
for (int i = mShortcuts.size() - 1; i >= 0; i--) {
final ShortcutInfo si = mShortcuts.valueAt(i);
+ if (si.isDynamic()) {
+ if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) {
+ Slog.w(TAG, String.format(
+ "%s is no longer main activity. Disabling shorcut %s.",
+ getPackageName(), si.getId()));
+ if (disableDynamicWithId(si.getId())) {
+ continue; // Actually removed.
+ }
+ // Still pinned, so fall-through and possibly update the resources.
+ }
+ changed = true;
+ }
+
if (si.hasAnyResources()) {
if (!si.isOriginallyFromManifest()) {
if (publisherRes == null) {
@@ -912,9 +942,8 @@
final ComponentName newActivity = newShortcut.getActivity();
if (newActivity == null) {
if (operation != ShortcutService.OPERATION_UPDATE) {
- // This method may be called before validating shortcuts, so this may happen,
- // and is a caller side error.
- throw new NullPointerException("Activity must be provided");
+ service.wtf("Activity must not be null at this point");
+ continue; // Just ignore this invalid case.
}
continue; // Activity can be null for update.
}
diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java
index c349b75..858e1cd 100644
--- a/services/core/java/com/android/server/pm/ShortcutParser.java
+++ b/services/core/java/com/android/server/pm/ShortcutParser.java
@@ -21,6 +21,7 @@
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
@@ -58,18 +59,36 @@
@Nullable
public static List<ShortcutInfo> parseShortcuts(ShortcutService service,
String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException {
- final PackageInfo pi = service.injectGetActivitiesWithMetadata(packageName, userId);
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d",
+ packageName, userId));
+ }
+ final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId);
+ if (activities == null || activities.size() == 0) {
+ return null;
+ }
List<ShortcutInfo> result = null;
try {
- if (pi != null && pi.activities != null) {
- for (ActivityInfo activityInfo : pi.activities) {
- result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result);
+ final int size = activities.size();
+ for (int i = 0; i < size; i++) {
+ final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo;
+ if (activityInfoNoMetadata == null) {
+ continue;
+ }
+
+ final ActivityInfo activityInfoWithMetadata =
+ service.injectGetActivityInfoWithMetadata(
+ activityInfoNoMetadata.getComponentName(), userId);
+ if (activityInfoWithMetadata != null) {
+ result = parseShortcutsOneFile(
+ service, activityInfoWithMetadata, packageName, userId, result);
}
}
} catch (RuntimeException e) {
- // Resource ID mismatch may cause various runtime exceptions when parsing XMLs.
+ // Resource ID mismatch may cause various runtime exceptions when parsing XMLs,
+ // But we don't crash the device, so just swallow them.
service.wtf(
"Exception caught while parsing shortcut XML for package=" + packageName, e);
return null;
@@ -81,6 +100,11 @@
ShortcutService service,
ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
List<ShortcutInfo> result) throws IOException, XmlPullParserException {
+ if (ShortcutService.DEBUG) {
+ Slog.d(TAG, String.format(
+ "Checking main activity %s", activityInfo.getComponentName()));
+ }
+
XmlResourceParser parser = null;
try {
parser = service.injectXmlMetaData(activityInfo, METADATA_KEY);
@@ -223,7 +247,7 @@
continue;
}
- Log.w(TAG, "Unknown tag " + tag + " at depth " + depth);
+ ShortcutService.warnForInvalidTag(depth, tag);
}
} finally {
if (parser != null) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 9f40772..1db1ce7 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -60,6 +60,7 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SELinux;
+import android.os.ServiceManager;
import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -76,6 +77,7 @@
import android.util.SparseLongArray;
import android.util.TypedValue;
import android.util.Xml;
+import android.view.IWindowManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -129,7 +131,7 @@
* - Default launcher check does take a few ms. Worth caching.
*
* - Detect when already registered instances are passed to APIs again, which might break
- * internal bitmap handling.
+ * internal bitmap handling.
*
* - Add more call stats.
*/
@@ -181,6 +183,8 @@
private static final String ATTR_VALUE = "value";
+ private static final String LAUNCHER_INTENT_CATEGORY = Intent.CATEGORY_LAUNCHER;
+
@VisibleForTesting
interface ConfigConstants {
/**
@@ -282,7 +286,8 @@
private List<Integer> mDirtyUserIds = new ArrayList<>();
/**
- * A counter that increments every time the system locale changes. We keep track of it to reset
+ * A counter that increments every time the system locale changes. We keep track of it to
+ * reset
* throttling counters on the first call from each package after the last locale change.
*
* We need this mechanism because we can't do much in the locale change callback, which is
@@ -294,8 +299,8 @@
private static final int PACKAGE_MATCH_FLAGS =
PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
- | PackageManager.MATCH_UNINSTALLED_PACKAGES;
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_UNINSTALLED_PACKAGES;
// Stats
@VisibleForTesting
@@ -306,13 +311,15 @@
int GET_APPLICATION_INFO = 3;
int LAUNCHER_PERMISSION_CHECK = 4;
int CLEANUP_DANGLING_BITMAPS = 5;
- int GET_ACTIVITIES_WITH_METADATA = 6;
+ int GET_ACTIVITY_WITH_METADATA = 6;
int GET_INSTALLED_PACKAGES = 7;
int CHECK_PACKAGE_CHANGES = 8;
int GET_APPLICATION_RESOURCES = 9;
int RESOURCE_NAME_LOOKUP = 10;
+ int GET_LAUNCHER_ACTIVITY = 11;
+ int CHECK_LAUNCHER_ACTIVITY = 12;
- int COUNT = RESOURCE_NAME_LOOKUP + 1;
+ int COUNT = CHECK_LAUNCHER_ACTIVITY + 1;
}
final Object mStatLock = new Object();
@@ -335,9 +342,10 @@
OPERATION_SET,
OPERATION_ADD,
OPERATION_UPDATE
- })
+ })
@Retention(RetentionPolicy.SOURCE)
- @interface ShortcutOperation {}
+ @interface ShortcutOperation {
+ }
public ShortcutService(Context context) {
this(context, BackgroundThread.get().getLooper());
@@ -373,18 +381,22 @@
}
final private IUidObserver mUidObserver = new IUidObserver.Stub() {
- @Override public void onUidStateChanged(int uid, int procState) throws RemoteException {
+ @Override
+ public void onUidStateChanged(int uid, int procState) throws RemoteException {
handleOnUidStateChanged(uid, procState);
}
- @Override public void onUidGone(int uid) throws RemoteException {
+ @Override
+ public void onUidGone(int uid) throws RemoteException {
handleOnUidStateChanged(uid, ActivityManager.MAX_PROCESS_STATE);
}
- @Override public void onUidActive(int uid) throws RemoteException {
+ @Override
+ public void onUidActive(int uid) throws RemoteException {
}
- @Override public void onUidIdle(int uid) throws RemoteException {
+ @Override
+ public void onUidIdle(int uid) throws RemoteException {
}
};
@@ -555,11 +567,11 @@
final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
? (int) parser.getLong(
- ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
- DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
+ ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
+ DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
: (int) parser.getLong(
- ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
- DEFAULT_MAX_ICON_DIMENSION_DP));
+ ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
+ DEFAULT_MAX_ICON_DIMENSION_DP));
mMaxIconDimension = injectDipToPixel(iconDimensionDp);
@@ -777,7 +789,7 @@
}
} catch (FileNotFoundException e) {
// Use the default
- } catch (IOException|XmlPullParserException e) {
+ } catch (IOException | XmlPullParserException e) {
Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
mRawLastResetTime = 0;
@@ -800,7 +812,7 @@
saveUserInternalLocked(userId, os, /* forBackup= */ false);
file.finishWrite(os);
- } catch (XmlPullParserException|IOException e) {
+ } catch (XmlPullParserException | IOException e) {
Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
file.failWrite(os);
}
@@ -850,10 +862,10 @@
return null;
}
try {
- final ShortcutUser ret = loadUserInternal(userId, in, /* forBackup= */ false);
+ final ShortcutUser ret = loadUserInternal(userId, in, /* forBackup= */ false);
cleanupDanglingBitmapDirectoriesLocked(userId, ret);
return ret;
- } catch (IOException|XmlPullParserException e) {
+ } catch (IOException | XmlPullParserException e) {
Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
return null;
} finally {
@@ -1133,7 +1145,7 @@
}
final String baseName = String.valueOf(injectCurrentTimeMillis());
- for (int suffix = 0;; suffix++) {
+ for (int suffix = 0; ; suffix++) {
final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
final File file = new File(packagePath, filename);
if (!file.exists()) {
@@ -1205,7 +1217,7 @@
} finally {
IoUtils.closeQuietly(out);
}
- } catch (IOException|RuntimeException e) {
+ } catch (IOException | RuntimeException e) {
// STOPSHIP Change wtf to e
Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
if (path != null && path.exists()) {
@@ -1284,7 +1296,7 @@
private boolean isCallerSystem() {
final int callingUid = injectBinderCallingUid();
- return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
+ return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
}
private boolean isCallerShell() {
@@ -1349,7 +1361,7 @@
/**
* @throws IllegalArgumentException if {@code numShortcuts} is bigger than
- * {@link #getMaxActivityShortcuts()}.
+ * {@link #getMaxActivityShortcuts()}.
*/
void enforceMaxActivityShortcuts(int numShortcuts) {
if (numShortcuts > mMaxShortcuts) {
@@ -1402,7 +1414,7 @@
* Clean up / validate an incoming shortcut.
* - Make sure all mandatory fields are set.
* - Make sure the intent's extras are persistable, and them to set
- * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras.
+ * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras.
* - Clear flags.
*
* TODO Detailed unit tests
@@ -1412,11 +1424,15 @@
if (shortcut.getActivity() != null) {
Preconditions.checkState(
shortcut.getPackage().equals(shortcut.getActivity().getPackageName()),
- "Activity package name mismatch");
+ "Cannot publish shortcut: activity " + shortcut.getActivity() + " does not"
+ + " belong to package " + shortcut.getPackage());
}
if (!forUpdate) {
shortcut.enforceMandatoryFields();
+ Preconditions.checkArgument(
+ injectIsMainActivity(shortcut.getActivity(), shortcut.getUserId()),
+ "Cannot publish shortcut: " + shortcut.getActivity() + " is not main activity");
}
if (shortcut.getIcon() != null) {
ShortcutInfo.validateIcon(shortcut.getIcon());
@@ -1425,6 +1441,26 @@
shortcut.replaceFlags(0);
}
+ /**
+ * When a shortcut has no target activity, set the default one from the package.
+ */
+ private void fillInDefaultActivity(List<ShortcutInfo> shortcuts) {
+
+ ComponentName defaultActivity = null;
+ for (int i = shortcuts.size() - 1; i >= 0; i--) {
+ final ShortcutInfo si = shortcuts.get(i);
+ if (si.getActivity() == null) {
+ if (defaultActivity == null) {
+ defaultActivity = injectGetDefaultMainActivity(
+ si.getPackage(), si.getUserId());
+ Preconditions.checkState(defaultActivity != null,
+ "Launcher activity not found for package " + si.getPackage());
+ }
+ si.setActivity(defaultActivity);
+ }
+ }
+ }
+
private void assignImplicitRanks(List<ShortcutInfo> shortcuts) {
for (int i = shortcuts.size() - 1; i >= 0; i--) {
shortcuts.get(i).setImplicitRank(i);
@@ -1446,6 +1482,8 @@
ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
+ fillInDefaultActivity(newShortcuts);
+
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET);
// Throttling.
@@ -1493,6 +1531,9 @@
ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
+ // For update, don't fill in the default activity. Having null activity means
+ // "don't update the activity" here.
+
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_UPDATE);
// Throttling.
@@ -1573,6 +1614,8 @@
ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
+ fillInDefaultActivity(newShortcuts);
+
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD);
// Initialize the implicit ranks for ShortcutPackage.adjustRanks().
@@ -1795,7 +1838,8 @@
}
/**
- * Reset all throttling, for developer options and command line. Only system/shell can call it.
+ * Reset all throttling, for developer options and command line. Only system/shell can call
+ * it.
*/
@Override
public void resetThrottling() {
@@ -1917,10 +1961,12 @@
// === House keeping ===
- private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId) {
+ private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId,
+ boolean appStillExists) {
synchronized (mLock) {
forEachLoadedUserLocked(user ->
- cleanUpPackageLocked(packageName, user.getUserId(), packageUserId));
+ cleanUpPackageLocked(packageName, user.getUserId(), packageUserId,
+ appStillExists));
}
}
@@ -1932,7 +1978,8 @@
* This is called when an app is uninstalled, or an app gets "clear data"ed.
*/
@VisibleForTesting
- void cleanUpPackageLocked(String packageName, int owningUserId, int packageUserId) {
+ void cleanUpPackageLocked(String packageName, int owningUserId, int packageUserId,
+ boolean appStillExists) {
final boolean wasUserLoaded = isUserLoadedLocked(owningUserId);
final ShortcutUser user = getUserShortcutsLocked(owningUserId);
@@ -1961,6 +2008,13 @@
notifyListeners(packageName, owningUserId);
}
+ // If the app still exists (i.e. data cleared), we need to re-publish manifest shortcuts.
+ if (appStillExists && (packageUserId == owningUserId)) {
+ // This will do the notification and save when needed, so do it after the above
+ // notifyListeners.
+ user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ true);
+ }
+
if (!wasUserLoaded) {
// Note this will execute the scheduled save.
unloadUserLocked(owningUserId);
@@ -2269,19 +2323,29 @@
public void onPackageDataCleared(String packageName, int uid) {
handlePackageDataCleared(packageName, getChangingUserId());
}
+
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ handlePackageChanged(packageName, getChangingUserId());
+ return false; // We don't need to receive onSomePackagesChanged(), so just false.
+ }
};
/**
* Called when a user is unlocked.
* - Check all known packages still exist, and otherwise perform cleanup.
* - If a package still exists, check the version code. If it's been updated, may need to
- * update timestamps of its shortcuts.
+ * update timestamps of its shortcuts.
*/
@VisibleForTesting
void checkPackageChanges(@UserIdInt int ownerUserId) {
if (DEBUG) {
Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId);
}
+ if (injectIsSafeModeEnabled()) {
+ Slog.i(TAG, "Safe mode, skipping checkPackageChanges()");
+ return;
+ }
final long start = injectElapsedRealtime();
try {
@@ -2302,14 +2366,15 @@
if (gonePackages.size() > 0) {
for (int i = gonePackages.size() - 1; i >= 0; i--) {
final PackageWithUser pu = gonePackages.get(i);
- cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId);
+ cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId,
+ /* appStillExists = */ false);
}
}
final long now = injectCurrentTimeMillis();
// Then for each installed app, publish manifest shortcuts when needed.
forUpdatedPackages(ownerUserId, user.getLastAppScanTime(), ai -> {
- user.handlePackageAddedOrUpdated(ai.packageName);
+ user.handlePackageAddedOrUpdated(ai.packageName, /* forceRescan=*/ false);
});
// Write the time just before the scan, because there may be apps that have just
@@ -2330,7 +2395,7 @@
synchronized (mLock) {
final ShortcutUser user = getUserShortcutsLocked(userId);
user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
- user.handlePackageAddedOrUpdated(packageName);
+ user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ false);
}
verifyStates();
}
@@ -2345,7 +2410,7 @@
user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
if (isPackageInstalled(packageName, userId)) {
- user.handlePackageAddedOrUpdated(packageName);
+ user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ false);
}
}
verifyStates();
@@ -2356,7 +2421,7 @@
Slog.d(TAG, String.format("handlePackageRemoved: %s user=%d", packageName,
packageUserId));
}
- cleanUpPackageForAllLoadedUsers(packageName, packageUserId);
+ cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ false);
verifyStates();
}
@@ -2366,7 +2431,23 @@
Slog.d(TAG, String.format("handlePackageDataCleared: %s user=%d", packageName,
packageUserId));
}
- cleanUpPackageForAllLoadedUsers(packageName, packageUserId);
+ cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ true);
+
+ verifyStates();
+ }
+
+ private void handlePackageChanged(String packageName, int packageUserId) {
+ if (DEBUG) {
+ Slog.d(TAG, String.format("handlePackageChanged: %s user=%d", packageName,
+ packageUserId));
+ }
+
+ // Activities may be disabled or enabled. Just rescan the package.
+ synchronized (mLock) {
+ final ShortcutUser user = getUserShortcutsLocked(packageUserId);
+
+ user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ true);
+ }
verifyStates();
}
@@ -2405,7 +2486,7 @@
final long token = injectClearCallingIdentity();
try {
return mIPackageManager.getPackageInfo(packageName, PACKAGE_MATCH_FLAGS
- | (getSignatures ? PackageManager.GET_SIGNATURES : 0)
+ | (getSignatures ? PackageManager.GET_SIGNATURES : 0)
, userId);
} catch (RemoteException e) {
// Shouldn't happen.
@@ -2439,14 +2520,12 @@
}
@Nullable
- @VisibleForTesting
- PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
+ ActivityInfo injectGetActivityInfoWithMetadata(ComponentName activity, @UserIdInt int userId) {
final long start = injectElapsedRealtime();
final long token = injectClearCallingIdentity();
try {
- return mIPackageManager.getPackageInfo(packageName,
- PACKAGE_MATCH_FLAGS | PackageManager.GET_ACTIVITIES
- | PackageManager.GET_META_DATA, userId);
+ return mIPackageManager.getActivityInfo(activity,
+ PACKAGE_MATCH_FLAGS | PackageManager.GET_META_DATA, userId);
} catch (RemoteException e) {
// Shouldn't happen.
Slog.wtf(TAG, "RemoteException", e);
@@ -2454,7 +2533,7 @@
} finally {
injectRestoreCallingIdentity(token);
- logDurationStat(Stats.GET_ACTIVITIES_WITH_METADATA, start);
+ logDurationStat(Stats.GET_ACTIVITY_WITH_METADATA, start);
}
}
@@ -2531,6 +2610,86 @@
}
}
+ private Intent getMainActivityIntent() {
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(LAUNCHER_INTENT_CATEGORY);
+ return intent;
+ }
+
+ @Nullable
+ ComponentName injectGetDefaultMainActivity(@NonNull String packageName, int userId) {
+ final long start = injectElapsedRealtime();
+ final long token = injectClearCallingIdentity();
+ try {
+ final Intent intent = getMainActivityIntent();
+ intent.setPackage(packageName);
+
+ final List<ResolveInfo> resolved =
+ mContext.getPackageManager().queryIntentActivitiesAsUser(
+ intent, PACKAGE_MATCH_FLAGS, userId);
+
+ return (resolved == null || resolved.size() == 0)
+ ? null : resolved.get(0).activityInfo.getComponentName();
+ } finally {
+ injectRestoreCallingIdentity(token);
+
+ logDurationStat(Stats.GET_LAUNCHER_ACTIVITY, start);
+ }
+ }
+
+ boolean injectIsMainActivity(@NonNull ComponentName activity, int userId) {
+ final long start = injectElapsedRealtime();
+ final long token = injectClearCallingIdentity();
+ try {
+ final Intent intent = getMainActivityIntent();
+ intent.setPackage(activity.getPackageName());
+ intent.setComponent(activity);
+
+ final List<ResolveInfo> resolved =
+ mContext.getPackageManager().queryIntentActivitiesAsUser(
+ intent, PACKAGE_MATCH_FLAGS, userId);
+
+ return resolved != null && resolved.size() > 0;
+ } finally {
+ injectRestoreCallingIdentity(token);
+
+ logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start);
+ }
+ }
+
+ @NonNull
+ List<ResolveInfo> injectGetMainActivities(@NonNull String packageName, int userId) {
+ final long start = injectElapsedRealtime();
+ final long token = injectClearCallingIdentity();
+ try {
+ final Intent intent = getMainActivityIntent();
+ intent.setPackage(packageName);
+
+ final List<ResolveInfo> resolved =
+ mContext.getPackageManager().queryIntentActivitiesAsUser(
+ intent, PACKAGE_MATCH_FLAGS, userId);
+
+ return (resolved != null) ? resolved : new ArrayList<>(0);
+ } finally {
+ injectRestoreCallingIdentity(token);
+
+ logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start);
+ }
+ }
+
+ boolean injectIsSafeModeEnabled() {
+ final long token = injectClearCallingIdentity();
+ try {
+ return IWindowManager.Stub
+ .asInterface(ServiceManager.getService(Context.WINDOW_SERVICE))
+ .isSafeModeEnabled();
+ } catch (RemoteException e) {
+ return false; // Shouldn't happen though.
+ } finally {
+ injectRestoreCallingIdentity(token);
+ }
+ }
+
// === Backup & restore ===
boolean shouldBackupApp(String packageName, int userId) {
@@ -2560,7 +2719,7 @@
final ByteArrayOutputStream os = new ByteArrayOutputStream(32 * 1024);
try {
saveUserInternalLocked(userId, os, /* forBackup */ true);
- } catch (XmlPullParserException|IOException e) {
+ } catch (XmlPullParserException | IOException e) {
// Shouldn't happen.
Slog.w(TAG, "Backup failed.", e);
return null;
@@ -2579,7 +2738,7 @@
final ByteArrayInputStream is = new ByteArrayInputStream(payload);
try {
user = loadUserInternal(userId, is, /* fromBackup */ true);
- } catch (XmlPullParserException|IOException e) {
+ } catch (XmlPullParserException | IOException e) {
Slog.w(TAG, "Restoration failed.", e);
return;
}
@@ -2656,7 +2815,7 @@
pw.println(mResetInterval);
pw.print(" maxUpdatesPerInterval: ");
pw.println(mMaxUpdatesPerInterval);
- pw.print(" maxDynamicShortcuts: ");
+ pw.print(" maxShortcutsPerActivity: ");
pw.println(mMaxShortcuts);
pw.println();
@@ -2670,11 +2829,13 @@
dumpStatLS(pw, p, Stats.GET_PACKAGE_INFO_WITH_SIG, "getPackageInfo(SIG)");
dumpStatLS(pw, p, Stats.GET_APPLICATION_INFO, "getApplicationInfo");
dumpStatLS(pw, p, Stats.CLEANUP_DANGLING_BITMAPS, "cleanupDanglingBitmaps");
- dumpStatLS(pw, p, Stats.GET_ACTIVITIES_WITH_METADATA, "getActivities+metadata");
+ dumpStatLS(pw, p, Stats.GET_ACTIVITY_WITH_METADATA, "getActivity+metadata");
dumpStatLS(pw, p, Stats.GET_INSTALLED_PACKAGES, "getInstalledPackages");
dumpStatLS(pw, p, Stats.CHECK_PACKAGE_CHANGES, "checkPackageChanges");
dumpStatLS(pw, p, Stats.GET_APPLICATION_RESOURCES, "getApplicationResources");
dumpStatLS(pw, p, Stats.RESOURCE_NAME_LOOKUP, "resourceNameLookup");
+ dumpStatLS(pw, p, Stats.GET_LAUNCHER_ACTIVITY, "getLauncherActivity");
+ dumpStatLS(pw, p, Stats.CHECK_LAUNCHER_ACTIVITY, "checkLauncherActivity");
}
for (int i = 0; i < mUsers.size(); i++) {
@@ -2725,7 +2886,9 @@
enforceShell();
- (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
+ final int status = (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
+
+ resultReceiver.send(status, null);
}
static class CommandException extends Exception {
@@ -2796,6 +2959,9 @@
case "clear-shortcuts":
handleClearShortcuts();
break;
+ case "verify-states": // hidden command to verify various internal states.
+ handleVerifyStates();
+ break;
default:
return handleDefaultCommands(cmd);
}
@@ -2938,7 +3104,16 @@
Slog.i(TAG, "cmd: handleClearShortcuts: " + mUserId + ", " + packageName);
- ShortcutService.this.cleanUpPackageForAllLoadedUsers(packageName, mUserId);
+ ShortcutService.this.cleanUpPackageForAllLoadedUsers(packageName, mUserId,
+ /* appStillExists = */ true);
+ }
+
+ private void handleVerifyStates() throws CommandException {
+ try {
+ verifyStatesForce(); // This will throw when there's an issue.
+ } catch (Throwable th) {
+ throw new CommandException(th.getMessage() + "\n" + Log.getStackTraceString(th));
+ }
}
}
@@ -2978,7 +3153,7 @@
}
final void wtf(String message) {
- wtf( message, /* exception= */ null);
+ wtf(message, /* exception= */ null);
}
// Injection point.
@@ -3092,6 +3267,10 @@
}
}
+ private final void verifyStatesForce() {
+ verifyStatesInner();
+ }
+
private void verifyStatesInner() {
synchronized (this) {
forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates));
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index f8ee325..7ea89c9 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -88,7 +88,7 @@
@Override
public String toString() {
- return String.format("{Package: %d, %s}", userId, packageName);
+ return String.format("[Package: %d, %s]", userId, packageName);
}
}
@@ -99,8 +99,6 @@
private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
- private final SparseArray<ShortcutPackage> mPackagesFromUid = new SparseArray<>();
-
private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
/** Default launcher that can access the launcher apps APIs. */
@@ -244,12 +242,12 @@
}
}
- public void handlePackageAddedOrUpdated(@NonNull String packageName) {
+ public void handlePackageAddedOrUpdated(@NonNull String packageName, boolean forceRescan) {
final boolean isNewApp = !mPackages.containsKey(packageName);
final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName);
- if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp)) {
+ if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp, forceRescan)) {
if (isNewApp) {
mPackages.remove(packageName);
}
@@ -381,8 +379,10 @@
pw.print(mUserId);
pw.print(" Known locale seq#: ");
pw.print(mKnownLocaleChangeSequenceNumber);
- pw.print(" Last app scan: ");
+ pw.print(" Last app scan: [");
pw.print(mLastAppScanTime);
+ pw.print("] ");
+ pw.print(ShortcutService.formatTime(mLastAppScanTime));
pw.println();
prefix += prefix + " ";
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index bf56da3..71d1a3a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -52,6 +52,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutServiceInternal;
@@ -98,8 +99,11 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
protected static final String TAG = "ShortcutManagerTest";
@@ -114,6 +118,8 @@
protected static final String[] EMPTY_STRINGS = new String[0]; // Just for readability.
+ protected static final String MAIN_ACTIVITY_CLASS = "MainActivity";
+
// public for mockito
public class BaseContext extends MockContext {
@Override
@@ -311,8 +317,55 @@
}
@Override
- PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
- return mContext.injectGetActivitiesWithMetadata(packageName, userId);
+ ActivityInfo injectGetActivityInfoWithMetadata(ComponentName activity,
+ @UserIdInt int userId) {
+ final PackageInfo pi = mContext.injectGetActivitiesWithMetadata(
+ activity.getPackageName(), userId);
+ if (pi == null || pi.activities == null) {
+ return null;
+ }
+ for (ActivityInfo ai : pi.activities) {
+ if (!mEnabledActivityChecker.test(ai.getComponentName(), userId)) {
+ continue;
+ }
+ if (activity.equals(ai.getComponentName())) {
+ return ai;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ boolean injectIsMainActivity(@NonNull ComponentName activity, int userId) {
+ if (!mEnabledActivityChecker.test(activity, userId)) {
+ return false;
+ }
+ return mMainActivityChecker.test(activity, userId);
+ }
+
+ @Override
+ List<ResolveInfo> injectGetMainActivities(@NonNull String packageName, int userId) {
+ final PackageInfo pi = mContext.injectGetActivitiesWithMetadata(
+ packageName, userId);
+ if (pi == null || pi.activities == null) {
+ return null;
+ }
+ final ArrayList<ResolveInfo> ret = new ArrayList<>(pi.activities.length);
+ for (int i = 0; i < pi.activities.length; i++) {
+ if (!mEnabledActivityChecker.test(pi.activities[i].getComponentName(), userId)) {
+ continue;
+ }
+ final ResolveInfo ri = new ResolveInfo();
+ ri.activityInfo = pi.activities[i];
+ ret.add(ri);
+ }
+
+ return ret;
+ }
+
+ @Override
+ ComponentName injectGetDefaultMainActivity(@NonNull String packageName, int userId) {
+ return mMainActivityFetcher.apply(packageName, userId);
}
@Override
@@ -335,6 +388,11 @@
}
@Override
+ boolean injectIsSafeModeEnabled() {
+ return mSafeMode;
+ }
+
+ @Override
void wtf(String message, Exception e) {
// During tests, WTF is fatal.
fail(message + " exception: " + e);
@@ -441,6 +499,8 @@
protected File mInjectedFilePathRoot;
+ protected boolean mSafeMode;
+
protected long mInjectedCurrentTimeMillis;
protected boolean mInjectedIsLowRamDevice;
@@ -512,6 +572,15 @@
LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
|| LAUNCHER_3.equals(callingPackage) || LAUNCHER_4.equals(callingPackage);
+ protected BiPredicate<ComponentName, Integer> mMainActivityChecker =
+ (activity, userId) -> true;
+
+ protected BiFunction<String, Integer, ComponentName> mMainActivityFetcher =
+ (packageName, userId) -> new ComponentName(packageName, MAIN_ACTIVITY_CLASS);
+
+ protected BiPredicate<ComponentName, Integer> mEnabledActivityChecker
+ = (activity, userId) -> true; // all activities are enabled.
+
protected static final long START_TIME = 1440000000101L;
protected static final long INTERVAL = 10000;
@@ -1051,10 +1120,6 @@
+ "/" + ShortcutService.FILENAME_USER_PACKAGES, message);
}
- protected void waitOnMainThread() throws Throwable {
- runTestOnUiThread(() -> {});
- }
-
/**
* Make a shortcut with an ID.
*/
@@ -1410,6 +1475,13 @@
return i;
}
+ protected Intent genPackageChangedIntent(String pakcageName, int userId) {
+ Intent i = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+ i.setData(Uri.parse("package:" + pakcageName));
+ i.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+ return i;
+ }
+
protected Intent genPackageDataClear(String packageName, int userId) {
Intent i = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED);
i.setData(Uri.parse("package:" + packageName));
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index cec4782..7d33a30 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -40,6 +40,7 @@
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicShortcutCountExceeded;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertEmpty;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertForLauncherCallback;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.filterByActivity;
@@ -50,6 +51,7 @@
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.pfdToBitmap;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.resetAll;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.waitOnMainThread;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
@@ -84,6 +86,7 @@
import com.android.server.pm.ShortcutService.ConfigConstants;
import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
import com.android.server.pm.ShortcutUser.PackageWithUser;
+import com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.ShortcutListAsserter;
import org.mockito.ArgumentCaptor;
@@ -91,6 +94,9 @@
import java.io.IOException;
import java.util.List;
import java.util.Locale;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
/**
* Tests for ShortcutService and ShortcutManager.
@@ -347,6 +353,127 @@
});
}
+ public void testPublishWithNoActivity() {
+ // If activity is not explicitly set, use the default one.
+
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ // s1 and s3 has no activities.
+ final ShortcutInfo si1 = new ShortcutInfo.Builder(mClientContext, "si1")
+ .setShortLabel("label1")
+ .setIntent(new Intent("action1"))
+ .build();
+ final ShortcutInfo si2 = new ShortcutInfo.Builder(mClientContext, "si2")
+ .setShortLabel("label2")
+ .setActivity(new ComponentName(getCallingPackage(), "abc"))
+ .setIntent(new Intent("action2"))
+ .build();
+ final ShortcutInfo si3 = new ShortcutInfo.Builder(mClientContext, "si3")
+ .setShortLabel("label3")
+ .setIntent(new Intent("action3"))
+ .build();
+
+ // Set test 1
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+
+ assertWith(getCallerShortcuts())
+ .haveIds("si1")
+ .forShortcutWithId("si1", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ });
+
+ // Set test 2
+ assertTrue(mManager.setDynamicShortcuts(list(si2, si1)));
+
+ assertWith(getCallerShortcuts())
+ .haveIds("si1", "si2")
+ .forShortcutWithId("si1", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ })
+ .forShortcutWithId("si2", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ "abc"), si.getActivity());
+ });
+
+
+ // Set test 3
+ assertTrue(mManager.setDynamicShortcuts(list(si3, si1)));
+
+ assertWith(getCallerShortcuts())
+ .haveIds("si1", "si3")
+ .forShortcutWithId("si1", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ })
+ .forShortcutWithId("si3", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ });
+
+ mInjectedCurrentTimeMillis += INTERVAL; // reset throttling
+
+ // Add test 1
+ mManager.removeAllDynamicShortcuts();
+ assertTrue(mManager.addDynamicShortcuts(list(si1)));
+
+ assertWith(getCallerShortcuts())
+ .haveIds("si1")
+ .forShortcutWithId("si1", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ });
+
+ // Add test 2
+ mManager.removeAllDynamicShortcuts();
+ assertTrue(mManager.addDynamicShortcuts(list(si2, si1)));
+
+ assertWith(getCallerShortcuts())
+ .haveIds("si1", "si2")
+ .forShortcutWithId("si1", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ })
+ .forShortcutWithId("si2", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ "abc"), si.getActivity());
+ });
+
+
+ // Add test 3
+ mManager.removeAllDynamicShortcuts();
+ assertTrue(mManager.addDynamicShortcuts(list(si3, si1)));
+
+ assertWith(getCallerShortcuts())
+ .haveIds("si1", "si3")
+ .forShortcutWithId("si1", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ })
+ .forShortcutWithId("si3", si -> {
+ assertEquals(new ComponentName(getCallingPackage(),
+ MAIN_ACTIVITY_CLASS), si.getActivity());
+ });
+ });
+ }
+
+ public void testPublishWithNoActivity_noMainActivityInPackage() {
+ runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+ final ShortcutInfo si1 = new ShortcutInfo.Builder(mClientContext, "si1")
+ .setShortLabel("label1")
+ .setIntent(new Intent("action1"))
+ .build();
+
+ // Returning null means there's no main activity in this package.
+ mMainActivityFetcher = (packageName, userId) -> null;
+
+ assertExpectException(
+ RuntimeException.class, "Launcher activity not found for", () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(si1)));
+ });
+ });
+ }
+
public void testDeleteDynamicShortcuts() {
final ShortcutInfo si1 = makeShortcut("shortcut1");
final ShortcutInfo si2 = makeShortcut("shortcut2");
@@ -2336,151 +2463,87 @@
+ ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999"
);
- LauncherApps.Callback c0 = mock(LauncherApps.Callback.class);
+ setCaller(LAUNCHER_1, USER_0);
- // Set listeners
-
- runWithCaller(LAUNCHER_1, USER_0, () -> {
- mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper()));
- });
-
- runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
- assertTrue(mManager.setDynamicShortcuts(list(
- makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
- });
-
- waitOnMainThread();
- ArgumentCaptor<List> shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_1),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
.haveIds("s1", "s2", "s3")
.areAllWithKeyFieldsOnly()
.areAllDynamic();
// From different package.
- reset(c0);
- runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
- assertTrue(mManager.setDynamicShortcuts(list(
- makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
- });
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_2),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_2, HANDLE_USER_0)
.haveIds("s1", "s2", "s3")
.areAllWithKeyFieldsOnly()
.areAllDynamic();
// Different user, callback shouldn't be called.
- reset(c0);
- runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
- assertTrue(mManager.setDynamicShortcuts(list(
- makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
- });
- waitOnMainThread();
- verify(c0, times(0)).onShortcutsChanged(
- anyString(),
- any(List.class),
- any(UserHandle.class)
- );
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ }).assertNoCallbackCalled();
+
// Test for addDynamicShortcuts.
- reset(c0);
- runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
- dumpsysOnLogcat("before addDynamicShortcuts");
- assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s4"))));
- });
-
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_1),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s4"))));
+ });
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
.haveIds("s1", "s2", "s3", "s4")
.areAllWithKeyFieldsOnly()
.areAllDynamic();
// Test for remove
- reset(c0);
- runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
- mManager.removeDynamicShortcuts(list("s1"));
- });
-
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_1),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.removeDynamicShortcuts(list("s1"));
+ });
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
.haveIds("s2", "s3", "s4")
.areAllWithKeyFieldsOnly()
.areAllDynamic();
// Test for update
- reset(c0);
- runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
- assertTrue(mManager.updateShortcuts(list(
- makeShortcut("s1"), makeShortcut("s2"))));
- });
-
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_1),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.updateShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"))));
+ });
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
+ // All remaining shortcuts will be passed regardless of what's been updated.
.haveIds("s2", "s3", "s4")
.areAllWithKeyFieldsOnly()
.areAllDynamic();
// Test for deleteAll
- reset(c0);
- runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
- mManager.removeAllDynamicShortcuts();
- });
-
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_1),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.removeAllDynamicShortcuts();
+ });
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
.isEmpty();
// Update package1 with manifest shortcuts
- reset(c0);
- addManifestShortcutResource(
- new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
- R.xml.shortcut_2);
- updatePackageVersion(CALLING_PACKAGE_1, 1);
- mService.mPackageMonitor.onReceive(getTestContext(),
- genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
-
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_1),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
.areAllManifest()
.areAllWithKeyFieldsOnly()
.haveIds("ms1", "ms2");
@@ -2518,58 +2581,42 @@
mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
- reset(c0); // Check the callback for the next API call.
- runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
- mManager.removeDynamicShortcuts(list("s2"));
+ assertForLauncherCallback(mLauncherApps, () -> {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ mManager.removeDynamicShortcuts(list("s2"));
- assertWith(getCallerShortcuts())
- .haveIds("ms2", "s1", "s2")
+ assertWith(getCallerShortcuts())
+ .haveIds("ms2", "s1", "s2")
- .selectByIds("ms2")
- .areAllNotManifest()
- .areAllPinned()
- .areAllImmutable()
- .areAllDisabled()
+ .selectByIds("ms2")
+ .areAllNotManifest()
+ .areAllPinned()
+ .areAllImmutable()
+ .areAllDisabled()
- .revertToOriginalList()
- .selectByIds("s1")
- .areAllDynamic()
- .areAllNotPinned()
- .areAllEnabled()
+ .revertToOriginalList()
+ .selectByIds("s1")
+ .areAllDynamic()
+ .areAllNotPinned()
+ .areAllEnabled()
- .revertToOriginalList()
- .selectByIds("s2")
- .areAllNotDynamic()
- .areAllPinned()
- .areAllEnabled()
- ;
- });
-
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_1),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ .revertToOriginalList()
+ .selectByIds("s2")
+ .areAllNotDynamic()
+ .areAllPinned()
+ .areAllEnabled()
+ ;
+ });
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
.haveIds("ms2", "s1", "s2")
.areAllWithKeyFieldsOnly();
// Remove CALLING_PACKAGE_2
- reset(c0);
- uninstallPackage(USER_0, CALLING_PACKAGE_2);
- mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_0, USER_0);
-
- // Should get a callback with an empty list.
- waitOnMainThread();
- shortcuts = ArgumentCaptor.forClass(List.class);
- verify(c0).onShortcutsChanged(
- eq(CALLING_PACKAGE_2),
- shortcuts.capture(),
- eq(HANDLE_USER_0)
- );
- assertWith(shortcuts.getValue())
+ assertForLauncherCallback(mLauncherApps, () -> {
+ uninstallPackage(USER_0, CALLING_PACKAGE_2);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_0, USER_0,
+ /* appStillExists = */ false);
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_2, HANDLE_USER_0)
.isEmpty();
}
@@ -2970,7 +3017,7 @@
// Nonexistent package.
uninstallPackage(USER_0, "abc");
- mService.cleanUpPackageLocked("abc", USER_0, USER_0);
+ mService.cleanUpPackageLocked("abc", USER_0, USER_0, /* appStillExists = */ false);
// No changes.
assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
@@ -3002,7 +3049,8 @@
// Remove a package.
uninstallPackage(USER_0, CALLING_PACKAGE_1);
- mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_0, USER_0);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_0, USER_0,
+ /* appStillExists = */ false);
assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3033,7 +3081,7 @@
// Remove a launcher.
uninstallPackage(USER_10, LAUNCHER_1);
- mService.cleanUpPackageLocked(LAUNCHER_1, USER_10, USER_10);
+ mService.cleanUpPackageLocked(LAUNCHER_1, USER_10, USER_10, /* appStillExists = */ false);
assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3061,7 +3109,8 @@
// Remove a package.
uninstallPackage(USER_10, CALLING_PACKAGE_2);
- mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_10, USER_10);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_10, USER_10,
+ /* appStillExists = */ false);
assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3089,7 +3138,8 @@
// Remove the other launcher from user 10 too.
uninstallPackage(USER_10, LAUNCHER_2);
- mService.cleanUpPackageLocked(LAUNCHER_2, USER_10, USER_10);
+ mService.cleanUpPackageLocked(LAUNCHER_2, USER_10, USER_10,
+ /* appStillExists = */ false);
assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3117,7 +3167,8 @@
// More remove.
uninstallPackage(USER_10, CALLING_PACKAGE_1);
- mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_10, USER_10);
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_10, USER_10,
+ /* appStillExists = */ false);
assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3143,6 +3194,74 @@
mService.saveDirtyInfo();
}
+ public void testCleanupPackage_republishManifests() {
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+ });
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s2", "s3", "ms1", "ms2"), HANDLE_USER_0);
+ });
+
+ // Remove ms2 from manifest.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"))));
+
+ // Make sure the shortcuts are in the intended state.
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "s1", "s2", "s3")
+
+ .selectByIds("ms1")
+ .areAllManifest()
+ .areAllPinned()
+
+ .revertToOriginalList()
+ .selectByIds("ms2")
+ .areAllNotManifest()
+ .areAllPinned()
+
+ .revertToOriginalList()
+ .selectByIds("s1")
+ .areAllDynamic()
+ .areAllNotPinned()
+
+ .revertToOriginalList()
+ .selectByIds("s2")
+ .areAllDynamic()
+ .areAllPinned()
+
+ .revertToOriginalList()
+ .selectByIds("s3")
+ .areAllNotDynamic()
+ .areAllPinned();
+ });
+
+ // Clean up + re-publish manifests.
+ mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_0, USER_0,
+ /* appStillExists = */ true);
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1")
+ .areAllManifest();
+ });
+ }
+
public void testHandleGonePackage_crossProfile() {
// Create some shortcuts.
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
@@ -3454,6 +3573,20 @@
assertTrue(mManager.addDynamicShortcuts(list(
makeShortcutWithIcon("s1", bmp32x32), makeShortcutWithIcon("s2", bmp32x32)
)));
+ // Also add a manifest shortcut, which should be removed too.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_1);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("s1", "s2", "ms1")
+
+ .selectManifest()
+ .haveIds("ms1");
+ });
setCaller(CALLING_PACKAGE_2, USER_0);
assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
@@ -3629,8 +3762,47 @@
assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
}
- public void testHandlePackageUpdate() throws Throwable {
+ public void testHandlePackageClearData_manifestRepublished() {
+ // Add two manifests and two dynamics.
+ addManifestShortcutResource(
+ new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+ R.xml.shortcut_2);
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_10));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.addDynamicShortcuts(list(
+ makeShortcut("s1"), makeShortcut("s2"))));
+ });
+ runWithCaller(LAUNCHER_1, USER_10, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2", "s2"), HANDLE_USER_10);
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2", "s1", "s2")
+ .areAllEnabled()
+
+ .selectPinned()
+ .haveIds("ms2", "s2");
+ });
+
+ // Clear data
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageDataClear(CALLING_PACKAGE_1, USER_10));
+
+ // Only manifest shortcuts will remain, and are no longer pinned.
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms2")
+ .areAllEnabled()
+ .areAllNotPinned();
+ });
+ }
+
+ public void testHandlePackageUpdate() throws Throwable {
// Set up shortcuts and launchers.
final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
@@ -3894,6 +4066,202 @@
});
}
+ public void testHandlePackageChanged() {
+ final ComponentName ACTIVITY1 = new ComponentName(CALLING_PACKAGE_1, "act1");
+ final ComponentName ACTIVITY2 = new ComponentName(CALLING_PACKAGE_1, "act2");
+
+ addManifestShortcutResource(ACTIVITY1, R.xml.shortcut_1);
+ addManifestShortcutResource(ACTIVITY2, R.xml.shortcut_1_alt);
+
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageAddIntent(CALLING_PACKAGE_1, USER_10));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertTrue(mManager.addDynamicShortcuts(list(
+ makeShortcutWithActivity("s1", ACTIVITY1),
+ makeShortcutWithActivity("s2", ACTIVITY2)
+ )));
+ });
+ runWithCaller(LAUNCHER_1, USER_10, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms1-alt", "s2"), HANDLE_USER_10);
+ });
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms1-alt", "s1", "s2")
+ .areAllEnabled()
+
+ .selectPinned()
+ .haveIds("ms1-alt", "s2")
+
+ .revertToOriginalList()
+ .selectByIds("ms1", "s1")
+ .areAllWithActivity(ACTIVITY1)
+
+ .revertToOriginalList()
+ .selectByIds("ms1-alt", "s2")
+ .areAllWithActivity(ACTIVITY2)
+ ;
+ });
+
+ // First, no changes.
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms1-alt", "s1", "s2")
+ .areAllEnabled()
+
+ .selectPinned()
+ .haveIds("ms1-alt", "s2")
+
+ .revertToOriginalList()
+ .selectByIds("ms1", "s1")
+ .areAllWithActivity(ACTIVITY1)
+
+ .revertToOriginalList()
+ .selectByIds("ms1-alt", "s2")
+ .areAllWithActivity(ACTIVITY2)
+ ;
+ });
+
+ // Disable activity 1
+ mEnabledActivityChecker = (activity, userId) -> !ACTIVITY1.equals(activity);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1-alt", "s2")
+ .areAllEnabled()
+
+ .selectPinned()
+ .haveIds("ms1-alt", "s2")
+
+ .revertToOriginalList()
+ .selectByIds("ms1-alt", "s2")
+ .areAllWithActivity(ACTIVITY2)
+ ;
+ });
+
+ // Re-enable activity 1.
+ // Manifest shortcuts will be re-published, but dynamic ones are not.
+ mEnabledActivityChecker = (activity, userId) -> true;
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms1-alt", "s2")
+ .areAllEnabled()
+
+ .selectPinned()
+ .haveIds("ms1-alt", "s2")
+
+ .revertToOriginalList()
+ .selectByIds("ms1")
+ .areAllWithActivity(ACTIVITY1)
+
+ .revertToOriginalList()
+ .selectByIds("ms1-alt", "s2")
+ .areAllWithActivity(ACTIVITY2)
+ ;
+ });
+
+ // Disable activity 2
+ // Because "ms1-alt" and "s2" are both pinned, they will remain, but disabled.
+ mEnabledActivityChecker = (activity, userId) -> !ACTIVITY2.equals(activity);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+ runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("ms1", "ms1-alt", "s2")
+
+ .selectDynamic().isEmpty().revertToOriginalList() // no dynamics.
+
+ .selectPinned()
+ .haveIds("ms1-alt", "s2")
+ .areAllDisabled()
+
+ .revertToOriginalList()
+ .selectByIds("ms1")
+ .areAllWithActivity(ACTIVITY1)
+ .areAllEnabled()
+ ;
+ });
+ }
+
+ public void testHandlePackageUpdate_activityNoLongerMain() throws Throwable {
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertTrue(mManager.setDynamicShortcuts(list(
+ makeShortcutWithActivity("s1a",
+ new ComponentName(getCallingPackage(), "act1")),
+ makeShortcutWithActivity("s1b",
+ new ComponentName(getCallingPackage(), "act1")),
+ makeShortcutWithActivity("s2a",
+ new ComponentName(getCallingPackage(), "act2")),
+ makeShortcutWithActivity("s2b",
+ new ComponentName(getCallingPackage(), "act2")),
+ makeShortcutWithActivity("s3a",
+ new ComponentName(getCallingPackage(), "act3")),
+ makeShortcutWithActivity("s3b",
+ new ComponentName(getCallingPackage(), "act3"))
+ )));
+ assertWith(getCallerShortcuts())
+ .haveIds("s1a", "s1b", "s2a", "s2b", "s3a", "s3b")
+ .areAllDynamic();
+ });
+ runWithCaller(LAUNCHER_1, USER_0, () -> {
+ mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+ list("s1b", "s2b", "s3b"),
+ HANDLE_USER_0);
+ });
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ assertWith(getCallerShortcuts())
+ .haveIds("s1a", "s1b", "s2a", "s2b", "s3a", "s3b")
+ .areAllDynamic()
+
+ .selectByIds("s1b", "s2b", "s3b")
+ .areAllPinned();
+ });
+
+ // Update the app and act2 and act3 are no longer main.
+ mMainActivityChecker = (activity, userId) -> {
+ return activity.getClassName().equals("act1");
+ };
+
+ setCaller(LAUNCHER_1, USER_0);
+ assertForLauncherCallback(mLauncherApps, () -> {
+ updatePackageVersion(CALLING_PACKAGE_1, 1);
+ mService.mPackageMonitor.onReceive(getTestContext(),
+ genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+ }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
+ // Make sure the launcher gets callbacks.
+ .haveIds("s1a", "s1b", "s2b", "s3b")
+ .areAllWithKeyFieldsOnly();
+
+ runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+ // s2a and s3a are gone, but s2b and s3b will remain because they're pinned, and
+ // disabled.
+ assertWith(getCallerShortcuts())
+ .haveIds("s1a", "s1b", "s2b", "s3b")
+
+ .selectByIds("s1a", "s1b")
+ .areAllDynamic()
+ .areAllEnabled()
+
+ .revertToOriginalList()
+ .selectByIds("s2b", "s3b")
+ .areAllNotDynamic()
+ .areAllDisabled()
+ .areAllPinned()
+ ;
+ });
+ }
+
protected void prepareForBackupTest() {
prepareCrossProfileDataSet();
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index 2856866..f570ff2 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -72,18 +72,68 @@
"ID must be provided",
() -> new ShortcutInfo.Builder(getTestContext()).build());
- assertExpectException(NullPointerException.class, "Intent action must be set",
- () -> new ShortcutInfo.Builder(getTestContext()).setIntent(new Intent()));
-
- assertExpectException(NullPointerException.class, "Activity must be provided", () -> {
- ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()).setId("id").build();
- assertTrue(getManager().setDynamicShortcuts(list(si)));
- });
+ assertExpectException(
+ RuntimeException.class,
+ "id cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), null));
assertExpectException(
+ RuntimeException.class,
+ "id cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), ""));
+
+ assertExpectException(
+ RuntimeException.class,
+ "intent cannot be null",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(null));
+
+ assertExpectException(
+ RuntimeException.class,
+ "action must be set",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(new Intent()));
+
+ assertExpectException(
+ RuntimeException.class,
+ "activity cannot be null",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setActivity(null));
+
+ assertExpectException(
+ RuntimeException.class,
+ "shortLabel cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setShortLabel(null));
+
+ assertExpectException(
+ RuntimeException.class,
+ "shortLabel cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setShortLabel(""));
+
+ assertExpectException(
+ RuntimeException.class,
+ "longLabel cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setLongLabel(null));
+
+ assertExpectException(
+ RuntimeException.class,
+ "longLabel cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setLongLabel(""));
+
+ assertExpectException(
+ RuntimeException.class,
+ "disabledMessage cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setDisabledMessage(null));
+
+ assertExpectException(
+ RuntimeException.class,
+ "disabledMessage cannot be empty",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setDisabledMessage(""));
+
+ assertExpectException(NullPointerException.class, "action must be set",
+ () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(new Intent()));
+
+ // same for add.
+ assertExpectException(
IllegalArgumentException.class, "Short label must be provided", () -> {
- ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
- .setId("id")
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.build();
assertTrue(getManager().setDynamicShortcuts(list(si)));
@@ -91,25 +141,24 @@
assertExpectException(
IllegalArgumentException.class, "Short label must be provided", () -> {
- ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
- .setId("id")
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.build();
assertTrue(getManager().addDynamicShortcuts(list(si)));
});
+ // same for add.
assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
- ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
- .setId("id")
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.setShortLabel("x")
.build();
assertTrue(getManager().setDynamicShortcuts(list(si)));
});
+ // same for add.
assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
- ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
- .setId("id")
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.setShortLabel("x")
.build();
@@ -117,18 +166,17 @@
});
assertExpectException(
- IllegalStateException.class, "package name mismatch", () -> {
- ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
- .setId("id")
+ IllegalStateException.class, "does not belong to package", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setActivity(new ComponentName("xxx", "s"))
.build();
assertTrue(getManager().setDynamicShortcuts(list(si)));
});
+ // same for add.
assertExpectException(
- IllegalStateException.class, "package name mismatch", () -> {
- ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
- .setId("id")
+ IllegalStateException.class, "does not belong to package", () -> {
+ ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setActivity(new ComponentName("xxx", "s"))
.build();
assertTrue(getManager().addDynamicShortcuts(list(si)));
diff --git a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
index 4aa7590..7d7285a 100644
--- a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
+++ b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
@@ -24,9 +24,11 @@
import static junit.framework.Assert.fail;
import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyList;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -35,11 +37,14 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.Callback;
import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.BaseBundle;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -54,6 +59,7 @@
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.io.BufferedReader;
@@ -69,6 +75,7 @@
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
+import java.util.concurrent.CountDownLatch;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -664,8 +671,8 @@
}
private ShortcutListAsserter(ShortcutListAsserter original, List<ShortcutInfo> list) {
- mOriginal = original == null ? this : original;
- mList = new ArrayList<>(list);
+ mOriginal = (original == null) ? this : original;
+ mList = (list == null) ? new ArrayList<>(0) : new ArrayList<>(list);
}
public ShortcutListAsserter revertToOriginalList() {
@@ -813,6 +820,11 @@
return this;
}
+ public ShortcutListAsserter areAllWithActivity(ComponentName activity) {
+ forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.getActivity().equals(activity)));
+ return this;
+ }
+
public ShortcutListAsserter forAllShortcuts(Consumer<ShortcutInfo> sa) {
boolean found = false;
for (int i = 0; i < mList.size(); i++) {
@@ -902,4 +914,69 @@
}
}
}
+
+ public static void waitOnMainThread() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ new Handler(Looper.getMainLooper()).post(() -> latch.countDown());
+
+ latch.await();
+ }
+
+ public static class LauncherCallbackAsserter {
+ private final LauncherApps.Callback mCallback = mock(LauncherApps.Callback.class);
+
+ private Callback getMockCallback() {
+ return mCallback;
+ }
+
+ public LauncherCallbackAsserter assertNoCallbackCalled() {
+ verify(mCallback, times(0)).onShortcutsChanged(
+ anyString(),
+ any(List.class),
+ any(UserHandle.class));
+ return this;
+ }
+
+ public LauncherCallbackAsserter assertNoCallbackCalledForPackage(
+ String publisherPackageName) {
+ verify(mCallback, times(0)).onShortcutsChanged(
+ eq(publisherPackageName),
+ any(List.class),
+ any(UserHandle.class));
+ return this;
+ }
+
+ public LauncherCallbackAsserter assertNoCallbackCalledForPackageAndUser(
+ String publisherPackageName, UserHandle publisherUserHandle) {
+ verify(mCallback, times(0)).onShortcutsChanged(
+ eq(publisherPackageName),
+ any(List.class),
+ eq(publisherUserHandle));
+ return this;
+ }
+
+ public ShortcutListAsserter assertCallbackCalledForPackageAndUser(
+ String publisherPackageName, UserHandle publisherUserHandle) {
+ final ArgumentCaptor<List> shortcuts = ArgumentCaptor.forClass(List.class);
+ verify(mCallback, times(1)).onShortcutsChanged(
+ eq(publisherPackageName),
+ shortcuts.capture(),
+ eq(publisherUserHandle));
+ return new ShortcutListAsserter(shortcuts.getValue());
+ }
+ }
+
+ public static LauncherCallbackAsserter assertForLauncherCallback(
+ LauncherApps launcherApps, Runnable body) throws InterruptedException {
+ final LauncherCallbackAsserter asserter = new LauncherCallbackAsserter();
+ launcherApps.registerCallback(asserter.getMockCallback(),
+ new Handler(Looper.getMainLooper()));
+
+ body.run();
+
+ waitOnMainThread();
+
+ return asserter;
+ }
}