Merge "Shortcut: Only "main" activities can have shortcuts." into nyc-mr1-dev

This commit is contained in:
TreeHugger Robot
2016-06-24 21:36:36 +00:00
committed by Android (Google) Code Review
9 changed files with 1082 additions and 276 deletions

View File

@@ -709,7 +709,7 @@ public final class ShortcutInfo implements Parcelable {
@NonNull @NonNull
@Deprecated @Deprecated
public Builder setId(@NonNull String id) { public Builder setId(@NonNull String id) {
mId = Preconditions.checkStringNotEmpty(id, "id"); mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
return this; return this;
} }
@@ -721,14 +721,17 @@ public final class ShortcutInfo implements Parcelable {
*/ */
public Builder(Context context, String id) { public Builder(Context context, String id) {
mContext = context; 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. * 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 * <p>Only "main" activities -- i.e. ones with an intent filter for
* {@link ShortcutManager#updateShortcuts(List)}. * {@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 * <p>The package name of the target activity must match the package name of the shortcut
* publisher. * publisher.
@@ -738,7 +741,7 @@ public final class ShortcutInfo implements Parcelable {
*/ */
@NonNull @NonNull
public Builder setActivity(@NonNull ComponentName activity) { public Builder setActivity(@NonNull ComponentName activity) {
mActivity = Preconditions.checkNotNull(activity, "activity"); mActivity = Preconditions.checkNotNull(activity, "activity cannot be null");
return this; return this;
} }
@@ -785,7 +788,7 @@ public final class ShortcutInfo implements Parcelable {
@NonNull @NonNull
public Builder setShortLabel(@NonNull CharSequence shortLabel) { public Builder setShortLabel(@NonNull CharSequence shortLabel) {
Preconditions.checkState(mTitleResId == 0, "shortLabelResId already set"); Preconditions.checkState(mTitleResId == 0, "shortLabelResId already set");
mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel"); mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel cannot be empty");
return this; return this;
} }
@@ -810,7 +813,7 @@ public final class ShortcutInfo implements Parcelable {
@NonNull @NonNull
public Builder setLongLabel(@NonNull CharSequence longLabel) { public Builder setLongLabel(@NonNull CharSequence longLabel) {
Preconditions.checkState(mTextResId == 0, "longLabelResId already set"); Preconditions.checkState(mTextResId == 0, "longLabelResId already set");
mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel"); mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel cannot be empty");
return this; return this;
} }
@@ -854,7 +857,8 @@ public final class ShortcutInfo implements Parcelable {
Preconditions.checkState( Preconditions.checkState(
mDisabledMessageResId == 0, "disabledMessageResId already set"); mDisabledMessageResId == 0, "disabledMessageResId already set");
mDisabledMessage = mDisabledMessage =
Preconditions.checkStringNotEmpty(disabledMessage, "disabledMessage"); Preconditions.checkStringNotEmpty(disabledMessage,
"disabledMessage cannot be empty");
return this; return this;
} }
@@ -876,8 +880,8 @@ public final class ShortcutInfo implements Parcelable {
*/ */
@NonNull @NonNull
public Builder setIntent(@NonNull Intent intent) { public Builder setIntent(@NonNull Intent intent) {
mIntent = Preconditions.checkNotNull(intent, "intent"); mIntent = Preconditions.checkNotNull(intent, "intent cannot be null");
Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set"); Preconditions.checkNotNull(mIntent.getAction(), "intent's action must be set");
return this; return this;
} }
@@ -944,6 +948,11 @@ public final class ShortcutInfo implements Parcelable {
return mActivity; return mActivity;
} }
/** @hide */
public void setActivity(ComponentName activity) {
mActivity = activity;
}
/** /**
* Icon. * Icon.
* *

View File

@@ -54,8 +54,6 @@ import java.util.function.Predicate;
* User information used by {@link ShortcutService}. * User information used by {@link ShortcutService}.
* *
* All methods should be guarded by {@code #mShortcutUser.mService.mLock}. * All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
*
* TODO Max dynamic shortcuts cap should be per activity.
*/ */
class ShortcutPackage extends ShortcutPackageItem { class ShortcutPackage extends ShortcutPackageItem {
private static final String TAG = ShortcutService.TAG; 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 * 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. * 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) { public boolean deleteDynamicWithId(@NonNull String shortcutId) {
deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false); 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 @@ class ShortcutPackage extends ShortcutPackageItem {
* *
* @return TRUE if any shortcuts have been changed. * @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( final PackageInfo pi = mShortcutUser.mService.getPackageInfo(
getPackageName(), getPackageUserId()); getPackageName(), getPackageUserId());
if (pi == null) { if (pi == null) {
return false; // Shouldn't happen. return false; // Shouldn't happen.
} }
if (!isNewApp) { if (!isNewApp && !forceRescan) {
// Make sure the version code or last update time has changed. // Make sure the version code or last update time has changed.
// Otherwise, nothing to do. // Otherwise, nothing to do.
if (getPackageInfo().getVersionCode() >= pi.versionCode if (getPackageInfo().getVersionCode() >= pi.versionCode
@@ -649,12 +665,26 @@ class ShortcutPackage extends ShortcutPackageItem {
boolean changed = false; boolean changed = false;
// For existing shortcuts, update timestamps if they have any resources. // For existing shortcuts, update timestamps if they have any resources.
// Also check if shortcuts' activities are still main activities. Otherwise, disable them.
if (!isNewApp) { if (!isNewApp) {
Resources publisherRes = null; Resources publisherRes = null;
for (int i = mShortcuts.size() - 1; i >= 0; i--) { for (int i = mShortcuts.size() - 1; i >= 0; i--) {
final ShortcutInfo si = mShortcuts.valueAt(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.hasAnyResources()) {
if (!si.isOriginallyFromManifest()) { if (!si.isOriginallyFromManifest()) {
if (publisherRes == null) { if (publisherRes == null) {
@@ -912,9 +942,8 @@ class ShortcutPackage extends ShortcutPackageItem {
final ComponentName newActivity = newShortcut.getActivity(); final ComponentName newActivity = newShortcut.getActivity();
if (newActivity == null) { if (newActivity == null) {
if (operation != ShortcutService.OPERATION_UPDATE) { if (operation != ShortcutService.OPERATION_UPDATE) {
// This method may be called before validating shortcuts, so this may happen, service.wtf("Activity must not be null at this point");
// and is a caller side error. continue; // Just ignore this invalid case.
throw new NullPointerException("Activity must be provided");
} }
continue; // Activity can be null for update. continue; // Activity can be null for update.
} }

View File

@@ -21,6 +21,7 @@ import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.content.res.XmlResourceParser; import android.content.res.XmlResourceParser;
@@ -58,18 +59,36 @@ public class ShortcutParser {
@Nullable @Nullable
public static List<ShortcutInfo> parseShortcuts(ShortcutService service, public static List<ShortcutInfo> parseShortcuts(ShortcutService service,
String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException { 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; List<ShortcutInfo> result = null;
try { try {
if (pi != null && pi.activities != null) { final int size = activities.size();
for (ActivityInfo activityInfo : pi.activities) { for (int i = 0; i < size; i++) {
result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result); 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) { } 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( service.wtf(
"Exception caught while parsing shortcut XML for package=" + packageName, e); "Exception caught while parsing shortcut XML for package=" + packageName, e);
return null; return null;
@@ -81,6 +100,11 @@ public class ShortcutParser {
ShortcutService service, ShortcutService service,
ActivityInfo activityInfo, String packageName, @UserIdInt int userId, ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
List<ShortcutInfo> result) throws IOException, XmlPullParserException { List<ShortcutInfo> result) throws IOException, XmlPullParserException {
if (ShortcutService.DEBUG) {
Slog.d(TAG, String.format(
"Checking main activity %s", activityInfo.getComponentName()));
}
XmlResourceParser parser = null; XmlResourceParser parser = null;
try { try {
parser = service.injectXmlMetaData(activityInfo, METADATA_KEY); parser = service.injectXmlMetaData(activityInfo, METADATA_KEY);
@@ -223,7 +247,7 @@ public class ShortcutParser {
continue; continue;
} }
Log.w(TAG, "Unknown tag " + tag + " at depth " + depth); ShortcutService.warnForInvalidTag(depth, tag);
} }
} finally { } finally {
if (parser != null) { if (parser != null) {

View File

@@ -60,6 +60,7 @@ import android.os.Process;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.ResultReceiver; import android.os.ResultReceiver;
import android.os.SELinux; import android.os.SELinux;
import android.os.ServiceManager;
import android.os.ShellCommand; import android.os.ShellCommand;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.UserHandle; import android.os.UserHandle;
@@ -76,6 +77,7 @@ import android.util.SparseIntArray;
import android.util.SparseLongArray; import android.util.SparseLongArray;
import android.util.TypedValue; import android.util.TypedValue;
import android.util.Xml; import android.util.Xml;
import android.view.IWindowManager;
import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting; 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. * - Default launcher check does take a few ms. Worth caching.
* *
* - Detect when already registered instances are passed to APIs again, which might break * - Detect when already registered instances are passed to APIs again, which might break
* internal bitmap handling. * internal bitmap handling.
* *
* - Add more call stats. * - 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 ATTR_VALUE = "value";
private static final String LAUNCHER_INTENT_CATEGORY = Intent.CATEGORY_LAUNCHER;
@VisibleForTesting @VisibleForTesting
interface ConfigConstants { interface ConfigConstants {
/** /**
@@ -282,7 +286,8 @@ public class ShortcutService extends IShortcutService.Stub {
private List<Integer> mDirtyUserIds = new ArrayList<>(); 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. * 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 * 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 = private static final int PACKAGE_MATCH_FLAGS =
PackageManager.MATCH_DIRECT_BOOT_AWARE PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_UNINSTALLED_PACKAGES; | PackageManager.MATCH_UNINSTALLED_PACKAGES;
// Stats // Stats
@VisibleForTesting @VisibleForTesting
@@ -306,13 +311,15 @@ public class ShortcutService extends IShortcutService.Stub {
int GET_APPLICATION_INFO = 3; int GET_APPLICATION_INFO = 3;
int LAUNCHER_PERMISSION_CHECK = 4; int LAUNCHER_PERMISSION_CHECK = 4;
int CLEANUP_DANGLING_BITMAPS = 5; int CLEANUP_DANGLING_BITMAPS = 5;
int GET_ACTIVITIES_WITH_METADATA = 6; int GET_ACTIVITY_WITH_METADATA = 6;
int GET_INSTALLED_PACKAGES = 7; int GET_INSTALLED_PACKAGES = 7;
int CHECK_PACKAGE_CHANGES = 8; int CHECK_PACKAGE_CHANGES = 8;
int GET_APPLICATION_RESOURCES = 9; int GET_APPLICATION_RESOURCES = 9;
int RESOURCE_NAME_LOOKUP = 10; 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(); final Object mStatLock = new Object();
@@ -335,9 +342,10 @@ public class ShortcutService extends IShortcutService.Stub {
OPERATION_SET, OPERATION_SET,
OPERATION_ADD, OPERATION_ADD,
OPERATION_UPDATE OPERATION_UPDATE
}) })
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@interface ShortcutOperation {} @interface ShortcutOperation {
}
public ShortcutService(Context context) { public ShortcutService(Context context) {
this(context, BackgroundThread.get().getLooper()); this(context, BackgroundThread.get().getLooper());
@@ -373,18 +381,22 @@ public class ShortcutService extends IShortcutService.Stub {
} }
final private IUidObserver mUidObserver = new IUidObserver.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); 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); 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() final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
? (int) parser.getLong( ? (int) parser.getLong(
ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM, ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP) DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
: (int) parser.getLong( : (int) parser.getLong(
ConfigConstants.KEY_MAX_ICON_DIMENSION_DP, ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
DEFAULT_MAX_ICON_DIMENSION_DP)); DEFAULT_MAX_ICON_DIMENSION_DP));
mMaxIconDimension = injectDipToPixel(iconDimensionDp); mMaxIconDimension = injectDipToPixel(iconDimensionDp);
@@ -777,7 +789,7 @@ public class ShortcutService extends IShortcutService.Stub {
} }
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
// Use the default // Use the default
} catch (IOException|XmlPullParserException e) { } catch (IOException | XmlPullParserException e) {
Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
mRawLastResetTime = 0; mRawLastResetTime = 0;
@@ -800,7 +812,7 @@ public class ShortcutService extends IShortcutService.Stub {
saveUserInternalLocked(userId, os, /* forBackup= */ false); saveUserInternalLocked(userId, os, /* forBackup= */ false);
file.finishWrite(os); file.finishWrite(os);
} catch (XmlPullParserException|IOException e) { } catch (XmlPullParserException | IOException e) {
Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
file.failWrite(os); file.failWrite(os);
} }
@@ -850,10 +862,10 @@ public class ShortcutService extends IShortcutService.Stub {
return null; return null;
} }
try { try {
final ShortcutUser ret = loadUserInternal(userId, in, /* forBackup= */ false); final ShortcutUser ret = loadUserInternal(userId, in, /* forBackup= */ false);
cleanupDanglingBitmapDirectoriesLocked(userId, ret); cleanupDanglingBitmapDirectoriesLocked(userId, ret);
return ret; return ret;
} catch (IOException|XmlPullParserException e) { } catch (IOException | XmlPullParserException e) {
Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e); Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
return null; return null;
} finally { } finally {
@@ -1133,7 +1145,7 @@ public class ShortcutService extends IShortcutService.Stub {
} }
final String baseName = String.valueOf(injectCurrentTimeMillis()); 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 String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
final File file = new File(packagePath, filename); final File file = new File(packagePath, filename);
if (!file.exists()) { if (!file.exists()) {
@@ -1205,7 +1217,7 @@ public class ShortcutService extends IShortcutService.Stub {
} finally { } finally {
IoUtils.closeQuietly(out); IoUtils.closeQuietly(out);
} }
} catch (IOException|RuntimeException e) { } catch (IOException | RuntimeException e) {
// STOPSHIP Change wtf to e // STOPSHIP Change wtf to e
Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e); Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
if (path != null && path.exists()) { if (path != null && path.exists()) {
@@ -1284,7 +1296,7 @@ public class ShortcutService extends IShortcutService.Stub {
private boolean isCallerSystem() { private boolean isCallerSystem() {
final int callingUid = injectBinderCallingUid(); final int callingUid = injectBinderCallingUid();
return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID); return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
} }
private boolean isCallerShell() { private boolean isCallerShell() {
@@ -1349,7 +1361,7 @@ public class ShortcutService extends IShortcutService.Stub {
/** /**
* @throws IllegalArgumentException if {@code numShortcuts} is bigger than * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
* {@link #getMaxActivityShortcuts()}. * {@link #getMaxActivityShortcuts()}.
*/ */
void enforceMaxActivityShortcuts(int numShortcuts) { void enforceMaxActivityShortcuts(int numShortcuts) {
if (numShortcuts > mMaxShortcuts) { if (numShortcuts > mMaxShortcuts) {
@@ -1402,7 +1414,7 @@ public class ShortcutService extends IShortcutService.Stub {
* Clean up / validate an incoming shortcut. * Clean up / validate an incoming shortcut.
* - Make sure all mandatory fields are set. * - Make sure all mandatory fields are set.
* - Make sure the intent's extras are persistable, and them to 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. * - Clear flags.
* *
* TODO Detailed unit tests * TODO Detailed unit tests
@@ -1412,11 +1424,15 @@ public class ShortcutService extends IShortcutService.Stub {
if (shortcut.getActivity() != null) { if (shortcut.getActivity() != null) {
Preconditions.checkState( Preconditions.checkState(
shortcut.getPackage().equals(shortcut.getActivity().getPackageName()), 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) { if (!forUpdate) {
shortcut.enforceMandatoryFields(); shortcut.enforceMandatoryFields();
Preconditions.checkArgument(
injectIsMainActivity(shortcut.getActivity(), shortcut.getUserId()),
"Cannot publish shortcut: " + shortcut.getActivity() + " is not main activity");
} }
if (shortcut.getIcon() != null) { if (shortcut.getIcon() != null) {
ShortcutInfo.validateIcon(shortcut.getIcon()); ShortcutInfo.validateIcon(shortcut.getIcon());
@@ -1425,6 +1441,26 @@ public class ShortcutService extends IShortcutService.Stub {
shortcut.replaceFlags(0); 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) { private void assignImplicitRanks(List<ShortcutInfo> shortcuts) {
for (int i = shortcuts.size() - 1; i >= 0; i--) { for (int i = shortcuts.size() - 1; i >= 0; i--) {
shortcuts.get(i).setImplicitRank(i); shortcuts.get(i).setImplicitRank(i);
@@ -1446,6 +1482,8 @@ public class ShortcutService extends IShortcutService.Stub {
ps.ensureImmutableShortcutsNotIncluded(newShortcuts); ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
fillInDefaultActivity(newShortcuts);
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET); ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET);
// Throttling. // Throttling.
@@ -1493,6 +1531,9 @@ public class ShortcutService extends IShortcutService.Stub {
ps.ensureImmutableShortcutsNotIncluded(newShortcuts); 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); ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_UPDATE);
// Throttling. // Throttling.
@@ -1573,6 +1614,8 @@ public class ShortcutService extends IShortcutService.Stub {
ps.ensureImmutableShortcutsNotIncluded(newShortcuts); ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
fillInDefaultActivity(newShortcuts);
ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD); ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD);
// Initialize the implicit ranks for ShortcutPackage.adjustRanks(). // 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 @Override
public void resetThrottling() { public void resetThrottling() {
@@ -1917,10 +1961,12 @@ public class ShortcutService extends IShortcutService.Stub {
// === House keeping === // === House keeping ===
private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId) { private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId,
boolean appStillExists) {
synchronized (mLock) { synchronized (mLock) {
forEachLoadedUserLocked(user -> 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. * This is called when an app is uninstalled, or an app gets "clear data"ed.
*/ */
@VisibleForTesting @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 boolean wasUserLoaded = isUserLoadedLocked(owningUserId);
final ShortcutUser user = getUserShortcutsLocked(owningUserId); final ShortcutUser user = getUserShortcutsLocked(owningUserId);
@@ -1961,6 +2008,13 @@ public class ShortcutService extends IShortcutService.Stub {
notifyListeners(packageName, owningUserId); 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) { if (!wasUserLoaded) {
// Note this will execute the scheduled save. // Note this will execute the scheduled save.
unloadUserLocked(owningUserId); unloadUserLocked(owningUserId);
@@ -2269,19 +2323,29 @@ public class ShortcutService extends IShortcutService.Stub {
public void onPackageDataCleared(String packageName, int uid) { public void onPackageDataCleared(String packageName, int uid) {
handlePackageDataCleared(packageName, getChangingUserId()); 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. * Called when a user is unlocked.
* - Check all known packages still exist, and otherwise perform cleanup. * - 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 * - 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 @VisibleForTesting
void checkPackageChanges(@UserIdInt int ownerUserId) { void checkPackageChanges(@UserIdInt int ownerUserId) {
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId); Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId);
} }
if (injectIsSafeModeEnabled()) {
Slog.i(TAG, "Safe mode, skipping checkPackageChanges()");
return;
}
final long start = injectElapsedRealtime(); final long start = injectElapsedRealtime();
try { try {
@@ -2302,14 +2366,15 @@ public class ShortcutService extends IShortcutService.Stub {
if (gonePackages.size() > 0) { if (gonePackages.size() > 0) {
for (int i = gonePackages.size() - 1; i >= 0; i--) { for (int i = gonePackages.size() - 1; i >= 0; i--) {
final PackageWithUser pu = gonePackages.get(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(); final long now = injectCurrentTimeMillis();
// Then for each installed app, publish manifest shortcuts when needed. // Then for each installed app, publish manifest shortcuts when needed.
forUpdatedPackages(ownerUserId, user.getLastAppScanTime(), ai -> { 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 // 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) { synchronized (mLock) {
final ShortcutUser user = getUserShortcutsLocked(userId); final ShortcutUser user = getUserShortcutsLocked(userId);
user.attemptToRestoreIfNeededAndSave(this, packageName, userId); user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
user.handlePackageAddedOrUpdated(packageName); user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ false);
} }
verifyStates(); verifyStates();
} }
@@ -2345,7 +2410,7 @@ public class ShortcutService extends IShortcutService.Stub {
user.attemptToRestoreIfNeededAndSave(this, packageName, userId); user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
if (isPackageInstalled(packageName, userId)) { if (isPackageInstalled(packageName, userId)) {
user.handlePackageAddedOrUpdated(packageName); user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ false);
} }
} }
verifyStates(); verifyStates();
@@ -2356,7 +2421,7 @@ public class ShortcutService extends IShortcutService.Stub {
Slog.d(TAG, String.format("handlePackageRemoved: %s user=%d", packageName, Slog.d(TAG, String.format("handlePackageRemoved: %s user=%d", packageName,
packageUserId)); packageUserId));
} }
cleanUpPackageForAllLoadedUsers(packageName, packageUserId); cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ false);
verifyStates(); verifyStates();
} }
@@ -2366,7 +2431,23 @@ public class ShortcutService extends IShortcutService.Stub {
Slog.d(TAG, String.format("handlePackageDataCleared: %s user=%d", packageName, Slog.d(TAG, String.format("handlePackageDataCleared: %s user=%d", packageName,
packageUserId)); 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(); verifyStates();
} }
@@ -2405,7 +2486,7 @@ public class ShortcutService extends IShortcutService.Stub {
final long token = injectClearCallingIdentity(); final long token = injectClearCallingIdentity();
try { try {
return mIPackageManager.getPackageInfo(packageName, PACKAGE_MATCH_FLAGS return mIPackageManager.getPackageInfo(packageName, PACKAGE_MATCH_FLAGS
| (getSignatures ? PackageManager.GET_SIGNATURES : 0) | (getSignatures ? PackageManager.GET_SIGNATURES : 0)
, userId); , userId);
} catch (RemoteException e) { } catch (RemoteException e) {
// Shouldn't happen. // Shouldn't happen.
@@ -2439,14 +2520,12 @@ public class ShortcutService extends IShortcutService.Stub {
} }
@Nullable @Nullable
@VisibleForTesting ActivityInfo injectGetActivityInfoWithMetadata(ComponentName activity, @UserIdInt int userId) {
PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
final long start = injectElapsedRealtime(); final long start = injectElapsedRealtime();
final long token = injectClearCallingIdentity(); final long token = injectClearCallingIdentity();
try { try {
return mIPackageManager.getPackageInfo(packageName, return mIPackageManager.getActivityInfo(activity,
PACKAGE_MATCH_FLAGS | PackageManager.GET_ACTIVITIES PACKAGE_MATCH_FLAGS | PackageManager.GET_META_DATA, userId);
| PackageManager.GET_META_DATA, userId);
} catch (RemoteException e) { } catch (RemoteException e) {
// Shouldn't happen. // Shouldn't happen.
Slog.wtf(TAG, "RemoteException", e); Slog.wtf(TAG, "RemoteException", e);
@@ -2454,7 +2533,7 @@ public class ShortcutService extends IShortcutService.Stub {
} finally { } finally {
injectRestoreCallingIdentity(token); 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 === // === Backup & restore ===
boolean shouldBackupApp(String packageName, int userId) { boolean shouldBackupApp(String packageName, int userId) {
@@ -2560,7 +2719,7 @@ public class ShortcutService extends IShortcutService.Stub {
final ByteArrayOutputStream os = new ByteArrayOutputStream(32 * 1024); final ByteArrayOutputStream os = new ByteArrayOutputStream(32 * 1024);
try { try {
saveUserInternalLocked(userId, os, /* forBackup */ true); saveUserInternalLocked(userId, os, /* forBackup */ true);
} catch (XmlPullParserException|IOException e) { } catch (XmlPullParserException | IOException e) {
// Shouldn't happen. // Shouldn't happen.
Slog.w(TAG, "Backup failed.", e); Slog.w(TAG, "Backup failed.", e);
return null; return null;
@@ -2579,7 +2738,7 @@ public class ShortcutService extends IShortcutService.Stub {
final ByteArrayInputStream is = new ByteArrayInputStream(payload); final ByteArrayInputStream is = new ByteArrayInputStream(payload);
try { try {
user = loadUserInternal(userId, is, /* fromBackup */ true); user = loadUserInternal(userId, is, /* fromBackup */ true);
} catch (XmlPullParserException|IOException e) { } catch (XmlPullParserException | IOException e) {
Slog.w(TAG, "Restoration failed.", e); Slog.w(TAG, "Restoration failed.", e);
return; return;
} }
@@ -2656,7 +2815,7 @@ public class ShortcutService extends IShortcutService.Stub {
pw.println(mResetInterval); pw.println(mResetInterval);
pw.print(" maxUpdatesPerInterval: "); pw.print(" maxUpdatesPerInterval: ");
pw.println(mMaxUpdatesPerInterval); pw.println(mMaxUpdatesPerInterval);
pw.print(" maxDynamicShortcuts: "); pw.print(" maxShortcutsPerActivity: ");
pw.println(mMaxShortcuts); pw.println(mMaxShortcuts);
pw.println(); 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_PACKAGE_INFO_WITH_SIG, "getPackageInfo(SIG)");
dumpStatLS(pw, p, Stats.GET_APPLICATION_INFO, "getApplicationInfo"); dumpStatLS(pw, p, Stats.GET_APPLICATION_INFO, "getApplicationInfo");
dumpStatLS(pw, p, Stats.CLEANUP_DANGLING_BITMAPS, "cleanupDanglingBitmaps"); 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.GET_INSTALLED_PACKAGES, "getInstalledPackages");
dumpStatLS(pw, p, Stats.CHECK_PACKAGE_CHANGES, "checkPackageChanges"); dumpStatLS(pw, p, Stats.CHECK_PACKAGE_CHANGES, "checkPackageChanges");
dumpStatLS(pw, p, Stats.GET_APPLICATION_RESOURCES, "getApplicationResources"); dumpStatLS(pw, p, Stats.GET_APPLICATION_RESOURCES, "getApplicationResources");
dumpStatLS(pw, p, Stats.RESOURCE_NAME_LOOKUP, "resourceNameLookup"); 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++) { for (int i = 0; i < mUsers.size(); i++) {
@@ -2725,7 +2886,9 @@ public class ShortcutService extends IShortcutService.Stub {
enforceShell(); 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 { static class CommandException extends Exception {
@@ -2796,6 +2959,9 @@ public class ShortcutService extends IShortcutService.Stub {
case "clear-shortcuts": case "clear-shortcuts":
handleClearShortcuts(); handleClearShortcuts();
break; break;
case "verify-states": // hidden command to verify various internal states.
handleVerifyStates();
break;
default: default:
return handleDefaultCommands(cmd); return handleDefaultCommands(cmd);
} }
@@ -2938,7 +3104,16 @@ public class ShortcutService extends IShortcutService.Stub {
Slog.i(TAG, "cmd: handleClearShortcuts: " + mUserId + ", " + packageName); 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) { final void wtf(String message) {
wtf( message, /* exception= */ null); wtf(message, /* exception= */ null);
} }
// Injection point. // Injection point.
@@ -3092,6 +3267,10 @@ public class ShortcutService extends IShortcutService.Stub {
} }
} }
private final void verifyStatesForce() {
verifyStatesInner();
}
private void verifyStatesInner() { private void verifyStatesInner() {
synchronized (this) { synchronized (this) {
forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates)); forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates));

View File

@@ -88,7 +88,7 @@ class ShortcutUser {
@Override @Override
public String toString() { 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 ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
private final SparseArray<ShortcutPackage> mPackagesFromUid = new SparseArray<>();
private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>(); private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
/** Default launcher that can access the launcher apps APIs. */ /** 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 boolean isNewApp = !mPackages.containsKey(packageName);
final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName); final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName);
if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp)) { if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp, forceRescan)) {
if (isNewApp) { if (isNewApp) {
mPackages.remove(packageName); mPackages.remove(packageName);
} }
@@ -381,8 +379,10 @@ class ShortcutUser {
pw.print(mUserId); pw.print(mUserId);
pw.print(" Known locale seq#: "); pw.print(" Known locale seq#: ");
pw.print(mKnownLocaleChangeSequenceNumber); pw.print(mKnownLocaleChangeSequenceNumber);
pw.print(" Last app scan: "); pw.print(" Last app scan: [");
pw.print(mLastAppScanTime); pw.print(mLastAppScanTime);
pw.print("] ");
pw.print(ShortcutService.formatTime(mLastAppScanTime));
pw.println(); pw.println();
prefix += prefix + " "; prefix += prefix + " ";

View File

@@ -52,6 +52,7 @@ import android.content.pm.LauncherApps.ShortcutQuery;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal; import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager; import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutServiceInternal; import android.content.pm.ShortcutServiceInternal;
@@ -98,8 +99,11 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BiPredicate; import java.util.function.BiPredicate;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
protected static final String TAG = "ShortcutManagerTest"; 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[] EMPTY_STRINGS = new String[0]; // Just for readability.
protected static final String MAIN_ACTIVITY_CLASS = "MainActivity";
// public for mockito // public for mockito
public class BaseContext extends MockContext { public class BaseContext extends MockContext {
@Override @Override
@@ -311,8 +317,55 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
} }
@Override @Override
PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) { ActivityInfo injectGetActivityInfoWithMetadata(ComponentName activity,
return mContext.injectGetActivitiesWithMetadata(packageName, userId); @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 @Override
@@ -334,6 +387,11 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
} }
} }
@Override
boolean injectIsSafeModeEnabled() {
return mSafeMode;
}
@Override @Override
void wtf(String message, Exception e) { void wtf(String message, Exception e) {
// During tests, WTF is fatal. // During tests, WTF is fatal.
@@ -441,6 +499,8 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
protected File mInjectedFilePathRoot; protected File mInjectedFilePathRoot;
protected boolean mSafeMode;
protected long mInjectedCurrentTimeMillis; protected long mInjectedCurrentTimeMillis;
protected boolean mInjectedIsLowRamDevice; protected boolean mInjectedIsLowRamDevice;
@@ -512,6 +572,15 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage) LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
|| LAUNCHER_3.equals(callingPackage) || LAUNCHER_4.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 START_TIME = 1440000000101L;
protected static final long INTERVAL = 10000; protected static final long INTERVAL = 10000;
@@ -1051,10 +1120,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
+ "/" + ShortcutService.FILENAME_USER_PACKAGES, message); + "/" + ShortcutService.FILENAME_USER_PACKAGES, message);
} }
protected void waitOnMainThread() throws Throwable {
runTestOnUiThread(() -> {});
}
/** /**
* Make a shortcut with an ID. * Make a shortcut with an ID.
*/ */
@@ -1410,6 +1475,13 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
return i; 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) { protected Intent genPackageDataClear(String packageName, int userId) {
Intent i = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED); Intent i = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED);
i.setData(Uri.parse("package:" + packageName)); i.setData(Uri.parse("package:" + packageName));

View File

@@ -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.assertDynamicShortcutCountExceeded;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertEmpty; 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.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.assertShortcutIds;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith; import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.filterByActivity; 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.pfdToBitmap;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.resetAll; 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.set;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.waitOnMainThread;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt; 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.ConfigConstants;
import com.android.server.pm.ShortcutService.FileOutputStreamWithPath; import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
import com.android.server.pm.ShortcutUser.PackageWithUser; import com.android.server.pm.ShortcutUser.PackageWithUser;
import com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.ShortcutListAsserter;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -91,6 +94,9 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
/** /**
* Tests for ShortcutService and ShortcutManager. * 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() { public void testDeleteDynamicShortcuts() {
final ShortcutInfo si1 = makeShortcut("shortcut1"); final ShortcutInfo si1 = makeShortcut("shortcut1");
final ShortcutInfo si2 = makeShortcut("shortcut2"); final ShortcutInfo si2 = makeShortcut("shortcut2");
@@ -2336,151 +2463,87 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
+ ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999" + ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999"
); );
LauncherApps.Callback c0 = mock(LauncherApps.Callback.class); setCaller(LAUNCHER_1, USER_0);
// Set listeners assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
runWithCaller(LAUNCHER_1, USER_0, () -> { assertTrue(mManager.setDynamicShortcuts(list(
mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper())); makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
}); });
}).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_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())
.haveIds("s1", "s2", "s3") .haveIds("s1", "s2", "s3")
.areAllWithKeyFieldsOnly() .areAllWithKeyFieldsOnly()
.areAllDynamic(); .areAllDynamic();
// From different package. // From different package.
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> { runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
assertTrue(mManager.setDynamicShortcuts(list( assertTrue(mManager.setDynamicShortcuts(list(
makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3")))); makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
}); });
waitOnMainThread(); }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_2, HANDLE_USER_0)
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_2),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.haveIds("s1", "s2", "s3") .haveIds("s1", "s2", "s3")
.areAllWithKeyFieldsOnly() .areAllWithKeyFieldsOnly()
.areAllDynamic(); .areAllDynamic();
// Different user, callback shouldn't be called. // Different user, callback shouldn't be called.
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
assertTrue(mManager.setDynamicShortcuts(list( assertTrue(mManager.setDynamicShortcuts(list(
makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3")))); makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
}); });
waitOnMainThread(); }).assertNoCallbackCalled();
verify(c0, times(0)).onShortcutsChanged(
anyString(),
any(List.class),
any(UserHandle.class)
);
// Test for addDynamicShortcuts. // Test for addDynamicShortcuts.
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
dumpsysOnLogcat("before addDynamicShortcuts"); assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s4"))));
assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s4")))); });
}); }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
waitOnMainThread();
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_1),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.haveIds("s1", "s2", "s3", "s4") .haveIds("s1", "s2", "s3", "s4")
.areAllWithKeyFieldsOnly() .areAllWithKeyFieldsOnly()
.areAllDynamic(); .areAllDynamic();
// Test for remove // Test for remove
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
mManager.removeDynamicShortcuts(list("s1")); mManager.removeDynamicShortcuts(list("s1"));
}); });
}).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
waitOnMainThread();
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_1),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.haveIds("s2", "s3", "s4") .haveIds("s2", "s3", "s4")
.areAllWithKeyFieldsOnly() .areAllWithKeyFieldsOnly()
.areAllDynamic(); .areAllDynamic();
// Test for update // Test for update
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
assertTrue(mManager.updateShortcuts(list( assertTrue(mManager.updateShortcuts(list(
makeShortcut("s1"), makeShortcut("s2")))); makeShortcut("s1"), makeShortcut("s2"))));
}); });
}).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
waitOnMainThread(); // All remaining shortcuts will be passed regardless of what's been updated.
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_1),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.haveIds("s2", "s3", "s4") .haveIds("s2", "s3", "s4")
.areAllWithKeyFieldsOnly() .areAllWithKeyFieldsOnly()
.areAllDynamic(); .areAllDynamic();
// Test for deleteAll // Test for deleteAll
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
mManager.removeAllDynamicShortcuts(); mManager.removeAllDynamicShortcuts();
}); });
}).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
waitOnMainThread();
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_1),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.isEmpty(); .isEmpty();
// Update package1 with manifest shortcuts // Update package1 with manifest shortcuts
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
addManifestShortcutResource( addManifestShortcutResource(
new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
R.xml.shortcut_2); R.xml.shortcut_2);
updatePackageVersion(CALLING_PACKAGE_1, 1); updatePackageVersion(CALLING_PACKAGE_1, 1);
mService.mPackageMonitor.onReceive(getTestContext(), mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
}).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
waitOnMainThread();
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_1),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.areAllManifest() .areAllManifest()
.areAllWithKeyFieldsOnly() .areAllWithKeyFieldsOnly()
.haveIds("ms1", "ms2"); .haveIds("ms1", "ms2");
@@ -2518,58 +2581,42 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
mService.mPackageMonitor.onReceive(getTestContext(), mService.mPackageMonitor.onReceive(getTestContext(),
genPackageAddIntent(CALLING_PACKAGE_1, USER_0)); genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
reset(c0); // Check the callback for the next API call. assertForLauncherCallback(mLauncherApps, () -> {
runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
mManager.removeDynamicShortcuts(list("s2")); mManager.removeDynamicShortcuts(list("s2"));
assertWith(getCallerShortcuts()) assertWith(getCallerShortcuts())
.haveIds("ms2", "s1", "s2") .haveIds("ms2", "s1", "s2")
.selectByIds("ms2") .selectByIds("ms2")
.areAllNotManifest() .areAllNotManifest()
.areAllPinned() .areAllPinned()
.areAllImmutable() .areAllImmutable()
.areAllDisabled() .areAllDisabled()
.revertToOriginalList() .revertToOriginalList()
.selectByIds("s1") .selectByIds("s1")
.areAllDynamic() .areAllDynamic()
.areAllNotPinned() .areAllNotPinned()
.areAllEnabled() .areAllEnabled()
.revertToOriginalList() .revertToOriginalList()
.selectByIds("s2") .selectByIds("s2")
.areAllNotDynamic() .areAllNotDynamic()
.areAllPinned() .areAllPinned()
.areAllEnabled() .areAllEnabled()
; ;
}); });
}).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
waitOnMainThread();
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_1),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.haveIds("ms2", "s1", "s2") .haveIds("ms2", "s1", "s2")
.areAllWithKeyFieldsOnly(); .areAllWithKeyFieldsOnly();
// Remove CALLING_PACKAGE_2 // Remove CALLING_PACKAGE_2
reset(c0); assertForLauncherCallback(mLauncherApps, () -> {
uninstallPackage(USER_0, CALLING_PACKAGE_2); uninstallPackage(USER_0, CALLING_PACKAGE_2);
mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_0, USER_0); mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_0, USER_0,
/* appStillExists = */ false);
// Should get a callback with an empty list. }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_2, HANDLE_USER_0)
waitOnMainThread();
shortcuts = ArgumentCaptor.forClass(List.class);
verify(c0).onShortcutsChanged(
eq(CALLING_PACKAGE_2),
shortcuts.capture(),
eq(HANDLE_USER_0)
);
assertWith(shortcuts.getValue())
.isEmpty(); .isEmpty();
} }
@@ -2970,7 +3017,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
// Nonexistent package. // Nonexistent package.
uninstallPackage(USER_0, "abc"); uninstallPackage(USER_0, "abc");
mService.cleanUpPackageLocked("abc", USER_0, USER_0); mService.cleanUpPackageLocked("abc", USER_0, USER_0, /* appStillExists = */ false);
// No changes. // No changes.
assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2), assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
@@ -3002,7 +3049,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
// Remove a package. // Remove a package.
uninstallPackage(USER_0, CALLING_PACKAGE_1); 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), assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet())); hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3033,7 +3081,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
// Remove a launcher. // Remove a launcher.
uninstallPackage(USER_10, LAUNCHER_1); 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), assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet())); hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3061,7 +3109,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
// Remove a package. // Remove a package.
uninstallPackage(USER_10, CALLING_PACKAGE_2); 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), assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet())); hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3089,7 +3138,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
// Remove the other launcher from user 10 too. // Remove the other launcher from user 10 too.
uninstallPackage(USER_10, LAUNCHER_2); 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), assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet())); hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3117,7 +3167,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
// More remove. // More remove.
uninstallPackage(USER_10, CALLING_PACKAGE_1); 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), assertEquals(set(CALLING_PACKAGE_2),
hashSet(user0.getAllPackagesForTest().keySet())); hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3143,6 +3194,74 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
mService.saveDirtyInfo(); 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() { public void testHandleGonePackage_crossProfile() {
// Create some shortcuts. // Create some shortcuts.
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
@@ -3454,6 +3573,20 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
assertTrue(mManager.addDynamicShortcuts(list( assertTrue(mManager.addDynamicShortcuts(list(
makeShortcutWithIcon("s1", bmp32x32), makeShortcutWithIcon("s2", bmp32x32) 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); setCaller(CALLING_PACKAGE_2, USER_0);
assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32)))); assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
@@ -3629,8 +3762,47 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10)); 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. // Set up shortcuts and launchers.
final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32); 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() { protected void prepareForBackupTest() {
prepareCrossProfileDataSet(); prepareCrossProfileDataSet();

View File

@@ -72,18 +72,68 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest {
"ID must be provided", "ID must be provided",
() -> new ShortcutInfo.Builder(getTestContext()).build()); () -> new ShortcutInfo.Builder(getTestContext()).build());
assertExpectException(NullPointerException.class, "Intent action must be set", assertExpectException(
() -> new ShortcutInfo.Builder(getTestContext()).setIntent(new Intent())); RuntimeException.class,
"id cannot be empty",
assertExpectException(NullPointerException.class, "Activity must be provided", () -> { () -> new ShortcutInfo.Builder(getTestContext(), null));
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( assertExpectException(
IllegalArgumentException.class, "Short label must be provided", () -> { IllegalArgumentException.class, "Short label must be provided", () -> {
ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()) ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setId("id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s")) .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.build(); .build();
assertTrue(getManager().setDynamicShortcuts(list(si))); assertTrue(getManager().setDynamicShortcuts(list(si)));
@@ -91,25 +141,24 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest {
assertExpectException( assertExpectException(
IllegalArgumentException.class, "Short label must be provided", () -> { IllegalArgumentException.class, "Short label must be provided", () -> {
ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()) ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setId("id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s")) .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.build(); .build();
assertTrue(getManager().addDynamicShortcuts(list(si))); assertTrue(getManager().addDynamicShortcuts(list(si)));
}); });
// same for add.
assertExpectException(NullPointerException.class, "Intent must be provided", () -> { assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()) ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setId("id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s")) .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.setShortLabel("x") .setShortLabel("x")
.build(); .build();
assertTrue(getManager().setDynamicShortcuts(list(si))); assertTrue(getManager().setDynamicShortcuts(list(si)));
}); });
// same for add.
assertExpectException(NullPointerException.class, "Intent must be provided", () -> { assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()) ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setId("id")
.setActivity(new ComponentName(getTestContext().getPackageName(), "s")) .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
.setShortLabel("x") .setShortLabel("x")
.build(); .build();
@@ -117,18 +166,17 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest {
}); });
assertExpectException( assertExpectException(
IllegalStateException.class, "package name mismatch", () -> { IllegalStateException.class, "does not belong to package", () -> {
ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()) ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setId("id")
.setActivity(new ComponentName("xxx", "s")) .setActivity(new ComponentName("xxx", "s"))
.build(); .build();
assertTrue(getManager().setDynamicShortcuts(list(si))); assertTrue(getManager().setDynamicShortcuts(list(si)));
}); });
// same for add.
assertExpectException( assertExpectException(
IllegalStateException.class, "package name mismatch", () -> { IllegalStateException.class, "does not belong to package", () -> {
ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()) ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
.setId("id")
.setActivity(new ComponentName("xxx", "s")) .setActivity(new ComponentName("xxx", "s"))
.build(); .build();
assertTrue(getManager().addDynamicShortcuts(list(si))); assertTrue(getManager().addDynamicShortcuts(list(si)));

View File

@@ -24,9 +24,11 @@ import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail; import static junit.framework.Assert.fail;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyList;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -35,11 +37,14 @@ import android.app.Instrumentation;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.pm.LauncherApps; import android.content.pm.LauncherApps;
import android.content.pm.LauncherApps.Callback;
import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.BaseBundle; import android.os.BaseBundle;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel; import android.os.Parcel;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle; import android.os.PersistableBundle;
@@ -54,6 +59,7 @@ import junit.framework.AssertionFailedError;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.io.BufferedReader; import java.io.BufferedReader;
@@ -69,6 +75,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
@@ -664,8 +671,8 @@ public class ShortcutManagerTestUtils {
} }
private ShortcutListAsserter(ShortcutListAsserter original, List<ShortcutInfo> list) { private ShortcutListAsserter(ShortcutListAsserter original, List<ShortcutInfo> list) {
mOriginal = original == null ? this : original; mOriginal = (original == null) ? this : original;
mList = new ArrayList<>(list); mList = (list == null) ? new ArrayList<>(0) : new ArrayList<>(list);
} }
public ShortcutListAsserter revertToOriginalList() { public ShortcutListAsserter revertToOriginalList() {
@@ -813,6 +820,11 @@ public class ShortcutManagerTestUtils {
return this; 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) { public ShortcutListAsserter forAllShortcuts(Consumer<ShortcutInfo> sa) {
boolean found = false; boolean found = false;
for (int i = 0; i < mList.size(); i++) { 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;
}
} }