diff options
| author | 2016-06-24 21:42:54 +0000 | |
|---|---|---|
| committer | 2016-06-24 21:42:54 +0000 | |
| commit | 312933e0439f36dbfdb51dba14c6ef31fe94bfca (patch) | |
| tree | 8f6d7d75ef108d8f900c7220ff0121e1f82b6541 | |
| parent | e37af56a6f0c78fc49e926d90dd65fc9d645c335 (diff) | |
| parent | 1af9a5895600469f2005554ecc346a53e9567b70 (diff) | |
Merge \"Shortcut: Only \"main\" activities can have shortcuts.\" into nyc-mr1-dev
am: 1af9a58956
Change-Id: Id878608aec49985228e129de9736b751c9ae8567
9 files changed, 1085 insertions, 279 deletions
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index a25ee3c81361..35370f0998fd 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -709,7 +709,7 @@ public final class ShortcutInfo implements Parcelable { @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 final class ShortcutInfo implements Parcelable { */ 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 @@ public final class ShortcutInfo implements Parcelable { */ @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 @@ public final class ShortcutInfo implements Parcelable { @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 @@ public final class ShortcutInfo implements Parcelable { @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 @@ public final class ShortcutInfo implements Parcelable { 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 @@ public final class ShortcutInfo implements Parcelable { */ @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 @@ public final class ShortcutInfo implements Parcelable { 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 d637586c8def..67e4e93fdbda 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 @@ import java.util.function.Predicate; * 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 @@ class ShortcutPackage extends ShortcutPackageItem { /** * 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 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. */ - public void deleteDynamicWithId(@NonNull String shortcutId) { - deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false); + private boolean disableDynamicWithId(@NonNull String shortcutId) { + final ShortcutInfo disabled = deleteOrDisableWithId( + shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false); + return disabled == null; } /** @@ -599,14 +615,14 @@ class ShortcutPackage extends ShortcutPackageItem { * * @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 @@ class ShortcutPackage extends ShortcutPackageItem { 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 @@ class ShortcutPackage extends ShortcutPackageItem { 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 c349b758b0ce..858e1cd9284b 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.ComponentName; 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 @@ public class ShortcutParser { @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 @@ public class ShortcutParser { 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 @@ public class ShortcutParser { 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 9f407723decd..1db1ce7432f4 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.Process; 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.SparseIntArray; 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 @@ import java.util.function.Predicate; * - 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 @@ public class ShortcutService extends IShortcutService.Stub { private static final String ATTR_VALUE = "value"; + private static final String LAUNCHER_INTENT_CATEGORY = Intent.CATEGORY_LAUNCHER; + @VisibleForTesting interface ConfigConstants { /** @@ -282,7 +286,8 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { } 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { } } 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { } 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 @@ public class ShortcutService extends IShortcutService.Stub { } 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { /** * @throws IllegalArgumentException if {@code numShortcuts} is bigger than - * {@link #getMaxActivityShortcuts()}. + * {@link #getMaxActivityShortcuts()}. */ void enforceMaxActivityShortcuts(int numShortcuts) { if (numShortcuts > mMaxShortcuts) { @@ -1402,7 +1414,7 @@ public class ShortcutService extends IShortcutService.Stub { * 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + fillInDefaultActivity(newShortcuts); + ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET); // Throttling. @@ -1493,6 +1531,9 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { ps.ensureImmutableShortcutsNotIncluded(newShortcuts); + fillInDefaultActivity(newShortcuts); + ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD); // Initialize the implicit ranks for ShortcutPackage.adjustRanks(). @@ -1795,7 +1838,8 @@ public class ShortcutService extends IShortcutService.Stub { } /** - * 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 @@ public class ShortcutService extends IShortcutService.Stub { // === 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 @@ public class ShortcutService extends IShortcutService.Stub { * 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { user.attemptToRestoreIfNeededAndSave(this, packageName, userId); if (isPackageInstalled(packageName, userId)) { - user.handlePackageAddedOrUpdated(packageName); + user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ false); } } verifyStates(); @@ -2356,7 +2421,7 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, String.format("handlePackageRemoved: %s user=%d", packageName, packageUserId)); } - cleanUpPackageForAllLoadedUsers(packageName, packageUserId); + cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ false); verifyStates(); } @@ -2366,7 +2431,23 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { } @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 @@ public class ShortcutService extends IShortcutService.Stub { } finally { injectRestoreCallingIdentity(token); - logDurationStat(Stats.GET_ACTIVITIES_WITH_METADATA, start); + logDurationStat(Stats.GET_ACTIVITY_WITH_METADATA, start); } } @@ -2531,6 +2610,86 @@ public class ShortcutService extends IShortcutService.Stub { } } + 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { pw.println(mResetInterval); pw.print(" maxUpdatesPerInterval: "); pw.println(mMaxUpdatesPerInterval); - pw.print(" maxDynamicShortcuts: "); + pw.print(" maxShortcutsPerActivity: "); pw.println(mMaxShortcuts); pw.println(); @@ -2670,11 +2829,13 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { 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 @@ public class ShortcutService extends IShortcutService.Stub { } final void wtf(String message) { - wtf( message, /* exception= */ null); + wtf(message, /* exception= */ null); } // Injection point. @@ -3092,6 +3267,10 @@ public class ShortcutService extends IShortcutService.Stub { } } + 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 f8ee3251055b..7ea89c9e8fcd 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 @@ class ShortcutUser { @Override public String toString() { - return String.format("{Package: %d, %s}", userId, packageName); + return String.format("[Package: %d, %s]", userId, packageName); } } @@ -99,8 +99,6 @@ class ShortcutUser { 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 @@ class ShortcutUser { } } - 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 @@ class ShortcutUser { 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 bf56da3218ae..71d1a3a64d19 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.LauncherApps.ShortcutQuery; 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.List; 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 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { 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 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { } @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 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { } @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 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected File mInjectedFilePathRoot; + protected boolean mSafeMode; + protected long mInjectedCurrentTimeMillis; protected boolean mInjectedIsLowRamDevice; @@ -512,6 +572,15 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { 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 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { + "/" + ShortcutService.FILENAME_USER_PACKAGES, message); } - protected void waitOnMainThread() throws Throwable { - runTestOnUiThread(() -> {}); - } - /** * Make a shortcut with an ID. */ @@ -1410,6 +1475,13 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { 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 cec478283922..7d33a3004e3b 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 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 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.frameworks.servicestests.R; 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.File; 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 class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } + 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { + ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999" ); - LauncherApps.Callback c0 = mock(LauncherApps.Callback.class); - - // Set listeners - - runWithCaller(LAUNCHER_1, USER_0, () -> { - mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper())); - }); + setCaller(LAUNCHER_1, USER_0); - 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()) + // Test for addDynamicShortcuts. + 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { 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")); - - assertWith(getCallerShortcuts()) - .haveIds("ms2", "s1", "s2") - - .selectByIds("ms2") - .areAllNotManifest() - .areAllPinned() - .areAllImmutable() - .areAllDisabled() - - .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()) + assertForLauncherCallback(mLauncherApps, () -> { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.removeDynamicShortcuts(list("s2")); + + assertWith(getCallerShortcuts()) + .haveIds("ms2", "s1", "s2") + + .selectByIds("ms2") + .areAllNotManifest() + .areAllPinned() + .areAllImmutable() + .areAllDisabled() + + .revertToOriginalList() + .selectByIds("s1") + .areAllDynamic() + .areAllNotPinned() + .areAllEnabled() + + .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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { 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 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { 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 class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } + 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 2856866bcbe7..f570ff24ce36 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 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { "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( + RuntimeException.class, + "id cannot be empty", + () -> new ShortcutInfo.Builder(getTestContext(), null)); - 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(), "")); + + 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 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { 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 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { }); 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 4aa7590b491a..7d7285a24a04 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.assertTrue; 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.app.Instrumentation; 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 junit.framework.AssertionFailedError; 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.List; 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 @@ public class ShortcutManagerTestUtils { } 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 @@ public class ShortcutManagerTestUtils { 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 class ShortcutManagerTestUtils { } } } + + 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; + } } |