Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 338430ce authored by Pinyao Ting's avatar Pinyao Ting
Browse files

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
parent 1d584f23
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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);
+44 −0
Original line number Diff line number Diff line
@@ -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
@@ -1384,6 +1400,22 @@ public final class ShortcutInfo implements Parcelable {
            return this;
        }

        /**
         * 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.
         */
@@ -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);
+49 −28
Original line number Diff line number Diff line
@@ -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,10 +160,18 @@ 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
     */
@@ -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));
        }

        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));
        }

        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 -> {
+4 −0
Original line number Diff line number Diff line
@@ -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)));
    }
+26 −0
Original line number Diff line number Diff line
@@ -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));

Loading