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

Commit b41bc495 authored by Pinyao Ting's avatar Pinyao Ting
Browse files

Shortcut integration with AppSearch (Part 5)

Following changes are implemented in this CL:
1. Persist shortcuts to icing whenever a shortcut is created/updated.
2. Delete shortcuts from icing only when publisher explicitly removes
the shortcut. i.e. deletion due to exceeding shortcut limit / disabling
shortcut do not lead to shortcut deletion from icing.
3. Call AppSearchSession#reportUsage when
ShortcutManger#pushDynamicShortcuts is invoked.
4. Add StrictMode warning for calling async method on mainthread.

Bug: 151359749
Test: atest ShortcutManagerTest1 ShortcutManagerTest2 ShortcutManagerTest3 ShortcutManagerTest4 ShortcutManagerTest5 ShortcutManagerTest6 ShortcutManagerTest7 ShortcutManagerTest8 ShortcutManagerTest9 ShortcutManagerTest10 ShortcutManagerTest11
Test: atest CtsShortcutManagerTestCases
Change-Id: I7ae7ec50a7e49f8de819d8239d429fc494017f96
parent 5f573843
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -327,6 +328,19 @@ public class AppSearchShortcutInfo extends GenericDocument {
        return si;
    }

    /**
     * @hide
     */
    @NonNull
    public static List<GenericDocument> toGenericDocuments(
            @NonNull final Collection<ShortcutInfo> shortcuts) {
        final List<GenericDocument> docs = new ArrayList<>(shortcuts.size());
        for (ShortcutInfo si : shortcuts) {
            docs.add(AppSearchShortcutInfo.instance(si));
        }
        return docs;
    }

    /** @hide */
    @VisibleForTesting
    public static class Builder extends GenericDocument.Builder<Builder> {
+476 −383

File changed.

Preview size limit exceeded, changes collapsed.

+11 −1
Original line number Diff line number Diff line
@@ -2590,7 +2590,6 @@ public class ShortcutService extends IShortcutService.Stub {

        final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId);
        ps.findAll(ret, query, cloneFlags);

        return new ParceledListSlice<>(setReturnedByServer(ret));
    }

@@ -5077,6 +5076,17 @@ public class ShortcutService extends IShortcutService.Stub {
        }
    }

    @VisibleForTesting
    void updatePackageShortcutForTest(String packageName, String shortcutId, int userId,
            Consumer<ShortcutInfo> cb) {
        synchronized (mLock) {
            final ShortcutPackage pkg = getPackageShortcutForTest(packageName, userId);
            if (pkg == null) return;

            pkg.mutateShortcut(shortcutId, null, cb);
        }
    }

    @VisibleForTesting
    ShortcutLauncher getLauncherShortcutForTest(String packageName, int userId) {
        synchronized (mLock) {
+15 −6
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSession;
import android.content.pm.ShortcutManager;
import android.metrics.LogMaker;
@@ -35,6 +34,7 @@ import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.server.FgThread;
@@ -715,17 +715,26 @@ class ShortcutUser {
                .setSubtype(totalSharingShortcutCount));
    }

    void runInAppSearch(@NonNull final AppSearchManager.SearchContext searchContext,
            @NonNull final Consumer<AppSearchResult<AppSearchSession>> callback) {
    AndroidFuture<AppSearchSession> getAppSearch(
            @NonNull final AppSearchManager.SearchContext searchContext) {
        final AndroidFuture<AppSearchSession> future = new AndroidFuture<>();
        if (mAppSearchManager == null) {
            Slog.e(TAG, "app search manager is null");
            return;
            future.completeExceptionally(new RuntimeException("app search manager is null"));
            return future;
        }
        final long callingIdentity = Binder.clearCallingIdentity();
        try {
            mAppSearchManager.createSearchSession(searchContext, mExecutor, callback);
            mAppSearchManager.createSearchSession(searchContext, mExecutor, result -> {
                if (!result.isSuccess()) {
                    future.completeExceptionally(
                            new RuntimeException(result.getErrorMessage()));
                    return;
                }
                future.complete(result.getResultValue());
            });
        } finally {
            Binder.restoreCallingIdentity(callingIdentity);
        }
        return future;
    }
}
+132 −17
Original line number Diff line number Diff line
@@ -48,10 +48,12 @@ import android.app.admin.DevicePolicyManager;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.IAppSearchBatchResultCallback;
import android.app.appsearch.IAppSearchManager;
import android.app.appsearch.IAppSearchResultCallback;
import android.app.appsearch.PackageIdentifier;
import android.app.appsearch.SearchResultPage;
import android.app.role.OnRoleHoldersChangedListener;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ActivityNotFoundException;
@@ -159,7 +161,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
                case Context.DEVICE_POLICY_SERVICE:
                    return mMockDevicePolicyManager;
                case Context.APP_SEARCH_SERVICE:
                    return new AppSearchManager(getTestContext(), mMockAppSearchManager);
                    return new AppSearchManager(this, mMockAppSearchManager);
                case Context.ROLE_SERVICE:
                    // RoleManager is final and cannot be mocked, so we only override the inject
                    // accessor methods in ShortcutService.
@@ -188,6 +190,12 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
            return getTestContext().getResources();
        }

        @Override
        public Context createContextAsUser(UserHandle user, int flags) {
            when(mMockPackageManager.getUserId()).thenReturn(user.getIdentifier());
            return this;
        }

        @Override
        public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
                IntentFilter filter, String broadcastPermission, Handler scheduler) {
@@ -195,12 +203,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
            return null;
        }

        @Override
        public Context createContextAsUser(UserHandle user, int flags) {
            when(mMockPackageManager.getUserId()).thenReturn(user.getIdentifier());
            return this;
        }

        @Override
        public void unregisterReceiver(BroadcastReceiver receiver) {
            // ignore.
@@ -237,6 +239,15 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
            mInjectedCallingUid = (int) token;
        }

        @Override
        public Context createContextAsUser(UserHandle user, int flags) {
            super.createContextAsUser(user, flags);
            final ServiceContext ctx = spy(new ServiceContext());
            when(ctx.getUser()).thenReturn(user);
            when(ctx.getUserId()).thenReturn(user.getIdentifier());
            return ctx;
        }

        @Override
        public int getUserId() {
            return UserHandle.USER_SYSTEM;
@@ -620,6 +631,11 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {

        protected Map<String, List<PackageIdentifier>> mSchemasPackageAccessible =
                new ArrayMap<>(1);
        private Map<String, Map<String, GenericDocument>> mDocumentMap = new ArrayMap<>(1);

        private String getKey(int userId, String databaseName) {
            return new StringBuilder().append(userId).append("@").append(databaseName).toString();
        }

        @Override
        public void setSchema(String packageName, String databaseName, List<Bundle> schemaBundles,
@@ -653,21 +669,77 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
        public void putDocuments(String packageName, String databaseName,
                List<Bundle> documentBundles, int userId, IAppSearchBatchResultCallback callback)
                throws RemoteException {
            ignore(callback);
            final List<GenericDocument> docs = new ArrayList<>(documentBundles.size());
            for (Bundle bundle : documentBundles) {
                docs.add(new GenericDocument(bundle));
            }
            final AppSearchBatchResult.Builder<String, Void> builder =
                    new AppSearchBatchResult.Builder<>();
            final String key = getKey(userId, databaseName);
            Map<String, GenericDocument> docMap = mDocumentMap.get(key);
            for (GenericDocument doc : docs) {
                builder.setSuccess(doc.getUri(), null);
                if (docMap == null) {
                    docMap = new ArrayMap<>(1);
                    mDocumentMap.put(key, docMap);
                }
                docMap.put(doc.getUri(), doc);
            }
            callback.onResult(builder.build());
        }

        @Override
        public void getDocuments(String packageName, String databaseName, String namespace,
                List<String> uris, Map<String, List<String>> typePropertyPaths, int userId,
                IAppSearchBatchResultCallback callback) throws RemoteException {
            ignore(callback);
            final AppSearchBatchResult.Builder<String, Bundle> builder =
                    new AppSearchBatchResult.Builder<>();
            final String key = getKey(userId, databaseName);
            if (!mDocumentMap.containsKey(key)) {
                for (String uri : uris) {
                    builder.setFailure(uri, AppSearchResult.RESULT_NOT_FOUND,
                            key + " not found when getting: " + uri);
                }
            } else {
                final Map<String, GenericDocument> docs = mDocumentMap.get(key);
                for (String uri : uris) {
                    if (docs.containsKey(uri)) {
                        builder.setSuccess(uri, docs.get(uri).getBundle());
                    } else {
                        builder.setFailure(uri, AppSearchResult.RESULT_NOT_FOUND,
                                "shortcut not found: " + uri);
                    }
                }
            }
            callback.onResult(builder.build());
        }

        @Override
        public void query(String packageName, String databaseName, String queryExpression,
                Bundle searchSpecBundle, int userId, IAppSearchResultCallback callback)
                throws RemoteException {
            ignore(callback);
            final String key = getKey(userId, databaseName);
            if (!mDocumentMap.containsKey(key)) {
                final Bundle page = new Bundle();
                page.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, 1);
                page.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, new ArrayList<>());
                callback.onResult(AppSearchResult.newSuccessfulResult(page));
                return;
            }
            final List<GenericDocument> documents = new ArrayList<>(mDocumentMap.get(key).values());
            final Bundle page = new Bundle();
            page.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, 0);
            final ArrayList<Bundle> resultBundles = new ArrayList<>();
            for (GenericDocument document : documents) {
                final Bundle resultBundle = new Bundle();
                resultBundle.putBundle("document", document.getBundle());
                resultBundle.putString("packageName", packageName);
                resultBundle.putString("databaseName", databaseName);
                resultBundle.putParcelableArrayList("matches", new ArrayList<>());
                resultBundles.add(resultBundle);
            }
            page.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
            callback.onResult(AppSearchResult.newSuccessfulResult(page));
        }

        @Override
@@ -679,7 +751,10 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
        @Override
        public void getNextPage(long nextPageToken, int userId, IAppSearchResultCallback callback)
                throws RemoteException {
            ignore(callback);
            final Bundle page = new Bundle();
            page.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, 1);
            page.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, new ArrayList<>());
            callback.onResult(AppSearchResult.newSuccessfulResult(page));
        }

        @Override
@@ -698,14 +773,40 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
        public void removeByUri(String packageName, String databaseName, String namespace,
                List<String> uris, int userId, IAppSearchBatchResultCallback callback)
                throws RemoteException {
            ignore(callback);
            final AppSearchBatchResult.Builder<String, Void> builder =
                    new AppSearchBatchResult.Builder<>();
            final String key = getKey(userId, databaseName);
            if (!mDocumentMap.containsKey(key)) {
                for (String uri : uris) {
                    builder.setFailure(uri, AppSearchResult.RESULT_NOT_FOUND,
                            "package " + key + " not found when removing " + uri);
                }
            } else {
                final Map<String, GenericDocument> docs = mDocumentMap.get(key);
                for (String uri : uris) {
                    if (docs.containsKey(uri)) {
                        docs.remove(uri);
                        builder.setSuccess(uri, null);
                    } else {
                        builder.setFailure(uri, AppSearchResult.RESULT_NOT_FOUND,
                                "shortcut not found when removing " + uri);
                    }
                }
            }
            callback.onResult(builder.build());
        }

        @Override
        public void removeByQuery(String packageName, String databaseName, String queryExpression,
                Bundle searchSpecBundle, int userId, IAppSearchResultCallback callback)
                throws RemoteException {
            ignore(callback);
            final String key = getKey(userId, databaseName);
            if (!mDocumentMap.containsKey(key)) {
                callback.onResult(AppSearchResult.newSuccessfulResult(null));
                return;
            }
            mDocumentMap.get(key).clear();
            callback.onResult(AppSearchResult.newSuccessfulResult(null));
        }

        @Override
@@ -724,12 +825,12 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
            return null;
        }

        private void ignore(IAppSearchResultCallback callback) throws RemoteException {
            callback.onResult(AppSearchResult.newSuccessfulResult(null));
        private void removeShortcuts() {
            mDocumentMap.clear();
        }

        private void ignore(IAppSearchBatchResultCallback callback) throws RemoteException {
            callback.onResult(new AppSearchBatchResult.Builder().build());
        private void ignore(IAppSearchResultCallback callback) throws RemoteException {
            callback.onResult(AppSearchResult.newSuccessfulResult(null));
        }
    }

@@ -1146,6 +1247,9 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {

        shutdownServices();

        mMockAppSearchManager.removeShortcuts();
        mMockAppSearchManager = null;

        super.tearDown();
    }

@@ -1891,6 +1995,11 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
        return mService.getPackageShortcutForTest(packageName, shortcutId, userId);
    }

    protected void updatePackageShortcut(String packageName, String shortcutId, int userId,
            Consumer<ShortcutInfo> cb) {
        mService.updatePackageShortcutForTest(packageName, shortcutId, userId, cb);
    }

    protected void assertShortcutExists(String packageName, String shortcutId, int userId) {
        assertTrue(getPackageShortcut(packageName, shortcutId, userId) != null);
    }
@@ -2086,6 +2195,10 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
        return getPackageShortcut(getCallingPackage(), shortcutId, getCallingUserId());
    }

    protected void updateCallerShortcut(String shortcutId, Consumer<ShortcutInfo> cb) {
        updatePackageShortcut(getCallingPackage(), shortcutId, getCallingUserId(), cb);
    }

    protected List<ShortcutInfo> getLauncherShortcuts(String launcher, int userId, int queryFlags) {
        final List<ShortcutInfo>[] ret = new List[1];
        runWithCaller(launcher, userId, () -> {
@@ -2245,6 +2358,8 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {

        deleteAllSavedFiles();

        mMockAppSearchManager.removeShortcuts();

        initService();
        mService.applyRestore(payload, USER_0);

Loading