diff options
| author | 2021-11-10 21:20:15 +0000 | |
|---|---|---|
| committer | 2021-12-11 04:30:18 +0000 | |
| commit | 338430cebb328ba1e7c183871e24005c20f18f55 (patch) | |
| tree | b02953ed6de5a8bb4a949ea67723a37decb4618a | |
| parent | 1d584f23e770a63267698a3401ff9d74488430ef (diff) | |
Include new api to exlude a shortcut from launcher
Include a new field in ShortcutInfo which serves as an indication of
whether a shortcut is exlucded from launcher. Shortcut marked as
excluded from launcher will not be included in the search result in
LauncherApps nor ShortcutManager. This generally means the shortcut
would not be displayed by a launcher app (e.g. Long-Press menu), while
remain visible in other surfaces such as assistant or
on-device-intelligence.
- setDynamicShortcuts/addDynamicShortcuts/pushDynamicShortcuts:
Shortcuts that are marked as hidden from launcher are ignored.
- updateShortcuts:
Similar to enabled/long-lived, developers cannot make shortcut hidden
from launcher by calling this api. An exception would be thrown when
updating a shortcut that is hidden from launcher.
- remove APIs:
Unchanged.
- reportShortcutUsed:
Unchanged.
- applyRestore:
Unchanged.
- disableShortcuts/enableShortcuts
Unchanged.
- requestPinShortcuts:
A shortcut cannot be pinned by launcher if it's hidden from launcher. An
exception would be thrown upon requesting to pin a shortcut that is
hidden from launcher.
Bug: 202335257
Test: manual enable feature flag for appsearch integration,
then run atest ShortcutManagerTest1
Change-Id: Ia0e5d31549c9d83efac9bc2a7ea894df425fd5cd
7 files changed, 190 insertions, 28 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 154c42f1565f..1e2197277d5c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -13385,6 +13385,7 @@ package android.content.pm { method public boolean isDynamic(); method public boolean isEnabled(); method public boolean isImmutable(); + method public boolean isIncludedIn(int); method public boolean isPinned(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.ShortcutInfo> CREATOR; @@ -13397,6 +13398,7 @@ package android.content.pm { field public static final int DISABLED_REASON_UNKNOWN = 3; // 0x3 field public static final int DISABLED_REASON_VERSION_LOWER = 100; // 0x64 field public static final String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation"; + field public static final int SURFACE_LAUNCHER = 1; // 0x1 } public static class ShortcutInfo.Builder { @@ -13405,6 +13407,7 @@ package android.content.pm { method @NonNull public android.content.pm.ShortcutInfo.Builder setActivity(@NonNull android.content.ComponentName); method @NonNull public android.content.pm.ShortcutInfo.Builder setCategories(java.util.Set<java.lang.String>); method @NonNull public android.content.pm.ShortcutInfo.Builder setDisabledMessage(@NonNull CharSequence); + method @NonNull public android.content.pm.ShortcutInfo.Builder setExcludedFromSurfaces(int); method @NonNull public android.content.pm.ShortcutInfo.Builder setExtras(@NonNull android.os.PersistableBundle); method @NonNull public android.content.pm.ShortcutInfo.Builder setIcon(android.graphics.drawable.Icon); method @NonNull public android.content.pm.ShortcutInfo.Builder setIntent(@NonNull android.content.Intent); diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index a264bebb5d88..613fb84812f8 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -352,6 +352,16 @@ public final class ShortcutInfo implements Parcelable { return disabledReason >= DISABLED_REASON_RESTORE_ISSUE_START; } + /** @hide */ + @IntDef(flag = true, value = {SURFACE_LAUNCHER}) + @Retention(RetentionPolicy.SOURCE) + public @interface Surface {} + + /** + * Indicates system surfaces managed by a launcher app. e.g. Long-Press Menu. + */ + public static final int SURFACE_LAUNCHER = 1 << 0; + /** * Shortcut category for messaging related actions, such as chat. */ @@ -451,6 +461,8 @@ public final class ShortcutInfo implements Parcelable { @Nullable private String mStartingThemeResName; + private int mExcludedSurfaces; + private ShortcutInfo(Builder b) { mUserId = b.mContext.getUserId(); @@ -474,6 +486,7 @@ public final class ShortcutInfo implements Parcelable { if (b.mIsLongLived) { setLongLived(); } + mExcludedSurfaces = b.mExcludedSurfaces; mRank = b.mRank; mExtras = b.mExtras; mLocusId = b.mLocusId; @@ -587,6 +600,7 @@ public final class ShortcutInfo implements Parcelable { mLastChangedTimestamp = source.mLastChangedTimestamp; mDisabledReason = source.mDisabledReason; mLocusId = source.mLocusId; + mExcludedSurfaces = source.mExcludedSurfaces; // Just always keep it since it's cheep. mIconResId = source.mIconResId; @@ -1025,6 +1039,8 @@ public final class ShortcutInfo implements Parcelable { private int mStartingThemeResId; + private int mExcludedSurfaces; + /** * Old style constructor. * @hide @@ -1385,6 +1401,22 @@ public final class ShortcutInfo implements Parcelable { } /** + * Sets which surfaces a shortcut will be excluded from. + * + * If the shortcut is set to be excluded from {@link #SURFACE_LAUNCHER}, shortcuts will be + * excluded from the search result of {@link android.content.pm.LauncherApps#getShortcuts( + * android.content.pm.LauncherApps.ShortcutQuery, UserHandle)} nor + * {@link android.content.pm.ShortcutManager#getShortcuts(int)}. This generally means the + * shortcut would not be displayed by a launcher app (e.g. in Long-Press menu), while + * remain visible in other surfaces such as assistant or on-device-intelligence. + */ + @NonNull + public Builder setExcludedFromSurfaces(final int surfaces) { + mExcludedSurfaces = surfaces; + return this; + } + + /** * Creates a {@link ShortcutInfo} instance. */ @NonNull @@ -2137,6 +2169,13 @@ public final class ShortcutInfo implements Parcelable { mCategories = cloneCategories(categories); } + /** + * Return true if the shortcut is included in specified surface. + */ + public boolean isIncludedIn(@Surface int surface) { + return (mExcludedSurfaces & surface) == 0; + } + private ShortcutInfo(Parcel source) { final ClassLoader cl = getClass().getClassLoader(); @@ -2185,6 +2224,7 @@ public final class ShortcutInfo implements Parcelable { mLocusId = source.readParcelable(cl); mIconUri = source.readString8(); mStartingThemeResName = source.readString8(); + mExcludedSurfaces = source.readInt(); } @Override @@ -2237,6 +2277,7 @@ public final class ShortcutInfo implements Parcelable { dest.writeParcelable(mLocusId, flags); dest.writeString8(mIconUri); dest.writeString8(mStartingThemeResName); + dest.writeInt(mExcludedSurfaces); } public static final @NonNull Creator<ShortcutInfo> CREATOR = @@ -2346,6 +2387,9 @@ public final class ShortcutInfo implements Parcelable { if (isLongLived()) { sb.append("Liv"); } + if (!isIncludedIn(SURFACE_LAUNCHER)) { + sb.append("Hid-L"); + } sb.append("]"); addIndentOrComma(sb, indent); diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 42c88b3eee8a..3d10b6f06488 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -96,8 +96,6 @@ import java.util.stream.Collectors; /** * Package information used by {@link ShortcutService}. * User information used by {@link ShortcutService}. - * - * All methods should be guarded by {@code #mShortcutUser.mService.mLock}. */ class ShortcutPackage extends ShortcutPackageItem { private static final String TAG = ShortcutService.TAG; @@ -162,11 +160,19 @@ class ShortcutPackage extends ShortcutPackageItem { private final Executor mExecutor; /** - * An temp in-memory copy of shortcuts for this package that was loaded from xml, keyed on IDs. + * An in-memory copy of shortcuts for this package that was loaded from xml, keyed on IDs. */ + @GuardedBy("mLock") final ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>(); /** + * A temporary copy of shortcuts that are to be cleared once persisted into AppSearch, keyed on + * IDs. + */ + @GuardedBy("mLock") + private ArrayMap<String, ShortcutInfo> mTransientShortcuts = new ArrayMap<>(0); + + /** * All the share targets from the package */ private final ArrayList<ShareTargetInfo> mShareTargets = new ArrayList<>(0); @@ -330,6 +336,15 @@ class ShortcutPackage extends ShortcutPackageItem { } } + public void ensureAllShortcutsVisibleToLauncher(@NonNull List<ShortcutInfo> shortcuts) { + for (ShortcutInfo shortcut : shortcuts) { + if (!shortcut.isIncludedIn(ShortcutInfo.SURFACE_LAUNCHER)) { + throw new IllegalArgumentException("Shortcut ID=" + shortcut.getId() + + " is hidden from launcher and may not be manipulated via APIs"); + } + } + } + /** * Delete a shortcut by ID. This will *always* remove it even if it's immutable or invisible. */ @@ -384,7 +399,15 @@ class ShortcutPackage extends ShortcutPackageItem { & (ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_CACHED_ALL)); } - forceReplaceShortcutInner(newShortcut); + if (!newShortcut.isIncludedIn(ShortcutInfo.SURFACE_LAUNCHER)) { + if (isAppSearchEnabled()) { + synchronized (mLock) { + mTransientShortcuts.put(newShortcut.getId(), newShortcut); + } + } + } else { + forceReplaceShortcutInner(newShortcut); + } return oldShortcut != null; } @@ -444,7 +467,15 @@ class ShortcutPackage extends ShortcutPackageItem { & (ShortcutInfo.FLAG_PINNED | ShortcutInfo.FLAG_CACHED_ALL)); } - forceReplaceShortcutInner(newShortcut); + if (!newShortcut.isIncludedIn(ShortcutInfo.SURFACE_LAUNCHER)) { + if (isAppSearchEnabled()) { + synchronized (mLock) { + mTransientShortcuts.put(newShortcut.getId(), newShortcut); + } + } + } else { + forceReplaceShortcutInner(newShortcut); + } if (isAppSearchEnabled()) { runAsSystem(() -> fromAppSearch().thenAccept(session -> session.reportUsage(new ReportUsageRequest.Builder( @@ -669,7 +700,6 @@ class ShortcutPackage extends ShortcutPackageItem { forEachShortcutMutate(si -> { if (!pinnedShortcuts.contains(si.getId()) && si.isPinned()) { si.clearFlags(ShortcutInfo.FLAG_PINNED); - return; } }); @@ -1704,8 +1734,15 @@ class ShortcutPackage extends ShortcutPackageItem { for (int j = 0; j < shareTargetSize; j++) { mShareTargets.get(j).saveToXml(out); } - saveShortcutsAsync(mShortcuts.values().stream().filter(ShortcutInfo::usesQuota) - .collect(Collectors.toList())); + synchronized (mLock) { + final Map<String, ShortcutInfo> copy = mShortcuts; + if (!mTransientShortcuts.isEmpty()) { + copy.putAll(mTransientShortcuts); + mTransientShortcuts.clear(); + } + saveShortcutsAsync(copy.values().stream().filter(ShortcutInfo::usesQuota).collect( + Collectors.toList())); + } } out.endTag(null, TAG_ROOT); @@ -2233,26 +2270,6 @@ class ShortcutPackage extends ShortcutPackageItem { } } - void updateVisibility(String packageName, byte[] certificate, boolean visible) { - if (!isAppSearchEnabled()) { - return; - } - if (visible) { - mPackageIdentifiers.put(packageName, new PackageIdentifier(packageName, certificate)); - } else { - mPackageIdentifiers.remove(packageName); - } - synchronized (mLock) { - mIsAppSearchSchemaUpToDate = false; - } - final long callingIdentity = Binder.clearCallingIdentity(); - try { - fromAppSearch(); - } finally { - Binder.restoreCallingIdentity(callingIdentity); - } - } - void mutateShortcut(@NonNull final String id, @Nullable final ShortcutInfo shortcut, @NonNull final Consumer<ShortcutInfo> transform) { Objects.requireNonNull(id); @@ -2358,6 +2375,7 @@ class ShortcutPackage extends ShortcutPackageItem { .addFilterSchemas(AppSearchShortcutInfo.SCHEMA_TYPE) .addFilterNamespaces(getPackageName()) .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) + .setResultCountPerPage(mShortcutUser.mService.getMaxActivityShortcuts()) .build(); } @@ -2451,6 +2469,9 @@ class ShortcutPackage extends ShortcutPackageItem { @VisibleForTesting void getTopShortcutsFromPersistence(AndroidFuture<List<ShortcutInfo>> cb) { + if (!isAppSearchEnabled()) { + cb.complete(null); + } runAsSystem(() -> fromAppSearch().thenAccept(session -> { SearchResults res = session.search("", getSearchSpec()); res.getNextPage(mShortcutUser.mExecutor, results -> { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 85b743594b75..a482f9a619ba 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -2014,6 +2014,7 @@ public class ShortcutService extends IShortcutService.Stub { ps.ensureImmutableShortcutsNotIncluded(newShortcuts, /*ignoreInvisible=*/ true); ps.ensureNoBitmapIconIfShortcutIsLongLived(newShortcuts); + ps.ensureAllShortcutsVisibleToLauncher(newShortcuts); // For update, don't fill in the default activity. Having null activity means // "don't update the activity" here. @@ -2214,6 +2215,9 @@ public class ShortcutService extends IShortcutService.Stub { IntentSender resultIntent, int userId, AndroidFuture<String> ret) { Objects.requireNonNull(shortcut); Preconditions.checkArgument(shortcut.isEnabled(), "Shortcut must be enabled"); + Preconditions.checkArgument( + shortcut.isIncludedIn(ShortcutInfo.SURFACE_LAUNCHER), + "Shortcut excluded from launcher cannot be pinned"); ret.complete(String.valueOf(requestPinItem( packageName, userId, shortcut, null, null, resultIntent))); } diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 58c9db76c282..ea7804d632a5 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -1500,6 +1500,20 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class), /* rank =*/ 0); } + /** + * Make a hidden shortcut with an ID. + */ + protected ShortcutInfo makeShortcutExcludedFromLauncher(String id) { + final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mClientContext, id) + .setActivity(new ComponentName(mClientContext.getPackageName(), "main")) + .setShortLabel("Title-" + id) + .setIntent(makeIntent(Intent.ACTION_VIEW, ShortcutActivity.class)) + .setExcludedFromSurfaces(ShortcutInfo.SURFACE_LAUNCHER); + final ShortcutInfo s = b.build(); + s.setTimestamp(mInjectedCurrentTimeMillis); + return s; + } + @Deprecated // Title was renamed to short label. protected ShortcutInfo makeShortcutWithTitle(String id, String title) { return makeShortcut( @@ -1889,6 +1903,18 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { assertEquals("Exception type different", expectedException, thrown.getClass()); } + protected void assertThrown(@NonNull final Class<?> expectedException, + @NonNull final Runnable fn) { + Exception thrown = null; + try { + fn.run(); + } catch (Exception e) { + thrown = e; + } + assertNotNull("Exception was not thrown", thrown); + assertEquals("Exception type different", expectedException, thrown.getClass()); + } + protected void assertBitmapDirectories(int userId, String... expectedDirectories) { final Set<String> expected = hashSet(set(expectedDirectories)); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index 32a88bd22986..a350dfbad662 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -8900,6 +8900,48 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { filter_any)); } + public void testAddingShortcuts_ExcludesHiddenFromLauncherShortcuts() { + final ShortcutInfo s1 = makeShortcutExcludedFromLauncher("s1"); + final ShortcutInfo s2 = makeShortcutExcludedFromLauncher("s2"); + final ShortcutInfo s3 = makeShortcutExcludedFromLauncher("s3"); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(s1, s2, s3))); + assertEmpty(mManager.getDynamicShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.addDynamicShortcuts(list(s1, s2, s3))); + assertEmpty(mManager.getDynamicShortcuts()); + }); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.pushDynamicShortcut(s1); + assertEmpty(mManager.getDynamicShortcuts()); + }); + } + + public void testUpdateShortcuts_ExcludesHiddenFromLauncherShortcuts() { + final ShortcutInfo s1 = makeShortcut("s1"); + final ShortcutInfo s2 = makeShortcut("s2"); + final ShortcutInfo s3 = makeShortcut("s3"); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertTrue(mManager.setDynamicShortcuts(list(s1, s2, s3))); + assertThrown(IllegalArgumentException.class, () -> { + mManager.updateShortcuts(list(makeShortcutExcludedFromLauncher("s1"))); + }); + }); + } + + public void testPinHiddenShortcuts_ThrowsException() { + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + assertThrown(IllegalArgumentException.class, () -> { + mManager.requestPinShortcut(makeShortcutExcludedFromLauncher("s1"), null); + }); + }); + } + private Uri getFileUriFromResource(String fileName, int resId) throws IOException { File file = new File(getTestContext().getFilesDir(), fileName); // Make sure we are not leaving phantom files behind. diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java index bcd216dd35d4..0708be2fb0c3 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java @@ -257,6 +257,28 @@ public class ShortcutManagerTest12 extends BaseShortcutManagerTest { assertEquals("custom", map.get("s3").getShortLabel()); } + public void testShortcutsExcludedFromLauncher_PersistedToDisk() { + if (!mService.isAppSearchEnabled()) { + return; + } + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.setDynamicShortcuts(list( + makeShortcutExcludedFromLauncher("s1"), + makeShortcutExcludedFromLauncher("s2"), + makeShortcutExcludedFromLauncher("s3"), + makeShortcutExcludedFromLauncher("s4"), + makeShortcutExcludedFromLauncher("s5") + )); + final List<ShortcutInfo> shortcuts = getAllPersistedShortcuts(); + assertNotNull(shortcuts); + assertEquals(5, shortcuts.size()); + final Map<String, ShortcutInfo> map = shortcuts.stream() + .collect(Collectors.toMap(ShortcutInfo::getId, Function.identity())); + assertTrue(map.containsKey("s3")); + assertEquals("Title-s3", map.get("s3").getShortLabel()); + } + + private List<ShortcutInfo> getAllPersistedShortcuts() { try { SystemClock.sleep(500); |