summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt3
-rw-r--r--core/java/android/content/pm/ShortcutInfo.java44
-rw-r--r--services/core/java/com/android/server/pm/ShortcutPackage.java77
-rw-r--r--services/core/java/com/android/server/pm/ShortcutService.java4
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java26
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java42
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java22
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);