diff options
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); |