diff options
| author | 2021-01-14 09:32:45 -0800 | |
|---|---|---|
| committer | 2021-02-24 14:55:00 -0800 | |
| commit | 85ad9ee3f7fb4feb747da9933cbdf8bfe26f68f6 (patch) | |
| tree | 311b631ea89080231334f903ac89075cf26ec75b | |
| parent | 01af9eec33b37edc1ad8bba4cb9022c2e31bfd73 (diff) | |
Shortcut integration with AppSearch (Part 1)
This CL includes the initial code change to integrate shortcuts with
AppSearch and provides a public api to allow publishers sharing access
to shortcuts with other apps.
Bug: 151359749
CTS-Coverage-Bug: 180558621
Test: manual
Change-Id: I08430dc3d49e4f2588c68c61561fffb2b248f4c9
8 files changed, 390 insertions, 0 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 0eb3b49ac8b1..5b48dadb000a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -12893,6 +12893,7 @@ package android.content.pm { method public void reportShortcutUsed(String); method public boolean requestPinShortcut(@NonNull android.content.pm.ShortcutInfo, @Nullable android.content.IntentSender); method public boolean setDynamicShortcuts(@NonNull java.util.List<android.content.pm.ShortcutInfo>); + method public void updateShortcutVisibility(@NonNull String, @Nullable byte[], boolean); method public boolean updateShortcuts(@NonNull java.util.List<android.content.pm.ShortcutInfo>); field public static final int FLAG_MATCH_CACHED = 8; // 0x8 field public static final int FLAG_MATCH_DYNAMIC = 2; // 0x2 diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl index 29a55b7a74da..b34574811bca 100644 --- a/core/java/android/content/pm/IShortcutService.aidl +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -78,4 +78,7 @@ interface IShortcutService { ParceledListSlice getShortcuts(String packageName, int matchFlags, int userId); void pushDynamicShortcut(String packageName, in ShortcutInfo shortcut, int userId); + + void updateShortcutVisibility(String callingPkg, String packageName, in byte[] certificate, + in boolean visible, int userId); }
\ No newline at end of file diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index 35c99a13a152..d3bac79aa2b9 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -39,6 +39,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import com.android.internal.annotations.VisibleForTesting; @@ -771,4 +772,20 @@ public class ShortcutManager { } } + /** + * Granting another app the access to the shortcuts you own. You must provide the package name + * and their SHA256 certificate digest in order to granting the access. + * + * Once granted, the other app can retain a copy of all the shortcuts you own when calling + * {@link LauncherApps#getShortcuts(LauncherApps.ShortcutQuery, UserHandle)}. + */ + public void updateShortcutVisibility(@NonNull final String packageName, + @Nullable final byte[] certificate, final boolean visible) { + try { + mService.updateShortcutVisibility(mContext.getPackageName(), packageName, certificate, + visible, injectMyUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index bb4ec16be0a8..a604afc22c09 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -19,15 +19,22 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.Person; +import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchSession; +import android.app.appsearch.PackageIdentifier; +import android.app.appsearch.SetSchemaRequest; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; import android.content.LocusId; +import android.content.pm.AppSearchPerson; +import android.content.pm.AppSearchShortcutInfo; import android.content.pm.PackageInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.graphics.drawable.Icon; +import android.os.Binder; import android.os.PersistableBundle; import android.text.format.Formatter; import android.util.ArrayMap; @@ -39,9 +46,11 @@ import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.Xml; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; +import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; import com.android.server.pm.ShortcutService.DumpFilter; @@ -64,8 +73,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; /** @@ -155,6 +168,16 @@ class ShortcutPackage extends ShortcutPackageItem { private long mLastKnownForegroundElapsedTime; + private final Object mLock = new Object(); + + /** + * All external packages that have gained access to the shortcuts from this package + */ + private final Map<String, PackageIdentifier> mPackageIdentifiers = new ArrayMap<>(0); + + @GuardedBy("mLock") + private AppSearchSession mAppSearchSession; + private ShortcutPackage(ShortcutUser shortcutUser, int packageUserId, String packageName, ShortcutPackageInfo spi) { super(shortcutUser, packageUserId, packageName, @@ -2140,6 +2163,15 @@ class ShortcutPackage extends ShortcutPackageItem { } } + void updateVisibility(String packageName, byte[] certificate, boolean visible) { + if (visible) { + mPackageIdentifiers.put(packageName, new PackageIdentifier(packageName, certificate)); + } else { + mPackageIdentifiers.remove(packageName); + } + resetAppSearch(null); + } + private boolean verifyRanksSequential(List<ShortcutInfo> list) { boolean failed = false; @@ -2153,4 +2185,128 @@ class ShortcutPackage extends ShortcutPackageItem { } return failed; } + + private void runInAppSearch( + Function<SearchSessionObservable, Consumer<AppSearchSession>>... observers) { + if (mShortcutUser == null) { + Slog.w(TAG, "shortcut user is null"); + return; + } + synchronized (mLock) { + if (mAppSearchSession != null) { + final CountDownLatch latch = new CountDownLatch(1); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + final SearchSessionObservable upstream = + new SearchSessionObservable(mAppSearchSession, latch); + for (Function<SearchSessionObservable, Consumer<AppSearchSession>> observer + : observers) { + upstream.map(observer); + } + upstream.next(); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + ConcurrentUtils.waitForCountDownNoInterrupt(latch, 500, + "timeout accessing shortcut"); + } else { + resetAppSearch(observers); + } + } + } + + private void resetAppSearch( + Function<SearchSessionObservable, Consumer<AppSearchSession>>... observers) { + final CountDownLatch latch = new CountDownLatch(1); + final AppSearchManager.SearchContext searchContext = + new AppSearchManager.SearchContext.Builder() + .setDatabaseName(getPackageName()).build(); + mShortcutUser.runInAppSearch(searchContext, result -> { + if (!result.isSuccess()) { + Slog.e(TAG, "error getting search session during lazy init, " + + result.getErrorMessage()); + latch.countDown(); + return; + } + // TODO: Flatten callback chain with proper async framework + final SearchSessionObservable upstream = + new SearchSessionObservable(result.getResultValue(), latch) + .map(this::setupSchema); + if (observers != null) { + for (Function<SearchSessionObservable, Consumer<AppSearchSession>> observer + : observers) { + upstream.map(observer); + } + } + upstream.map(observable -> session -> { + mAppSearchSession = session; + observable.next(); + }); + upstream.next(); + }); + ConcurrentUtils.waitForCountDownNoInterrupt(latch, 1500, + "timeout accessing shortcut during lazy initialization"); + } + + /** + * creates the schema for shortcut in the database + */ + private Consumer<AppSearchSession> setupSchema(SearchSessionObservable observable) { + return session -> { + SetSchemaRequest.Builder schemaBuilder = new SetSchemaRequest.Builder() + .addSchemas(AppSearchPerson.SCHEMA, AppSearchShortcutInfo.SCHEMA); + for (PackageIdentifier pi : mPackageIdentifiers.values()) { + schemaBuilder = schemaBuilder + .setSchemaTypeVisibilityForPackage( + AppSearchPerson.SCHEMA_TYPE, true, pi) + .setSchemaTypeVisibilityForPackage( + AppSearchShortcutInfo.SCHEMA_TYPE, true, pi); + } + session.setSchema(schemaBuilder.build(), mShortcutUser.mExecutor, result -> { + if (!result.isSuccess()) { + observable.error("failed to instantiate app search schema: " + + result.getErrorMessage()); + return; + } + observable.next(); + }); + }; + } + + /** + * TODO: Replace this temporary implementation with proper async framework + */ + private class SearchSessionObservable { + + final AppSearchSession mSession; + final CountDownLatch mLatch; + final ArrayList<Consumer<AppSearchSession>> mObservers = new ArrayList<>(1); + + SearchSessionObservable(@NonNull final AppSearchSession session, + @NonNull final CountDownLatch latch) { + mSession = session; + mLatch = latch; + } + + SearchSessionObservable map( + Function<SearchSessionObservable, Consumer<AppSearchSession>> observer) { + mObservers.add(observer.apply(this)); + return this; + } + + void next() { + if (mObservers.isEmpty()) { + mLatch.countDown(); + return; + } + mObservers.remove(0).accept(mSession); + } + + void error(@Nullable final String errorMessage) { + if (errorMessage != null) { + Slog.e(TAG, errorMessage); + } + mLatch.countDown(); + } + } } diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 4d8abea8acd4..209a143f665d 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -2150,6 +2150,15 @@ public class ShortcutService extends IShortcutService.Stub { } @Override + public void updateShortcutVisibility(String callingPkg, String packageName, byte[] certificate, + boolean visible, int userId) { + synchronized (mLock) { + getPackageShortcutsForPublisherLocked(callingPkg, userId) + .updateVisibility(packageName, certificate, visible); + } + } + + @Override public boolean requestPinShortcut(String packageName, ShortcutInfo shortcut, IntentSender resultIntent, int userId) { Objects.requireNonNull(shortcut); diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java index 3e3aa677912b..6cbc47fb59c4 100644 --- a/services/core/java/com/android/server/pm/ShortcutUser.java +++ b/services/core/java/com/android/server/pm/ShortcutUser.java @@ -18,9 +18,14 @@ package com.android.server.pm; 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; +import android.os.Binder; import android.os.FileUtils; +import android.os.UserHandle; import android.text.TextUtils; import android.text.format.Formatter; import android.util.ArrayMap; @@ -32,6 +37,7 @@ import android.util.TypedXmlSerializer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.server.FgThread; import com.android.server.pm.ShortcutService.DumpFilter; import com.android.server.pm.ShortcutService.InvalidFileFormatException; @@ -45,6 +51,7 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -111,6 +118,8 @@ class ShortcutUser { } final ShortcutService mService; + final AppSearchManager mAppSearchManager; + final Executor mExecutor; @UserIdInt private final int mUserId; @@ -132,6 +141,9 @@ class ShortcutUser { public ShortcutUser(ShortcutService service, int userId) { mService = service; mUserId = userId; + mAppSearchManager = service.mContext.createContextAsUser(UserHandle.of(userId), 0) + .getSystemService(AppSearchManager.class); + mExecutor = FgThread.getExecutor(); } public int getUserId() { @@ -693,4 +705,18 @@ class ShortcutUser { logger.write(logMaker.setType(MetricsEvent.SHORTCUTS_CHANGED_SHORTCUT_COUNT) .setSubtype(totalSharingShortcutCount)); } + + void runInAppSearch(@NonNull final AppSearchManager.SearchContext searchContext, + @NonNull final Consumer<AppSearchResult<AppSearchSession>> callback) { + if (mAppSearchManager == null) { + Slog.e(TAG, "app search manager is null"); + return; + } + final long callingIdentity = Binder.clearCallingIdentity(); + try { + mAppSearchManager.createSearchSession(searchContext, mExecutor, callback); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } } 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 e46ab6b01f0a..029e9a39ea4b 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -45,6 +45,13 @@ import android.app.ActivityManagerInternal; import android.app.IUidObserver; import android.app.Person; import android.app.admin.DevicePolicyManager; +import android.app.appsearch.AppSearchBatchResult; +import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchResult; +import android.app.appsearch.IAppSearchBatchResultCallback; +import android.app.appsearch.IAppSearchManager; +import android.app.appsearch.IAppSearchResultCallback; +import android.app.appsearch.PackageIdentifier; import android.app.role.OnRoleHoldersChangedListener; import android.app.usage.UsageStatsManagerInternal; import android.content.ActivityNotFoundException; @@ -78,6 +85,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.FileUtils; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.PersistableBundle; import android.os.Process; @@ -150,6 +158,8 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return mMockUserManager; case Context.DEVICE_POLICY_SERVICE: return mMockDevicePolicyManager; + case Context.APP_SEARCH_SERVICE: + return new AppSearchManager(getTestContext(), mMockAppSearchManager); case Context.ROLE_SERVICE: // RoleManager is final and cannot be mocked, so we only override the inject // accessor methods in ShortcutService. @@ -159,6 +169,11 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { } @Override + public String getOpPackageName() { + return getTestContext().getOpPackageName(); + } + + @Override public String getSystemServiceName(Class<?> serviceClass) { return getTestContext().getSystemServiceName(serviceClass); } @@ -601,6 +616,123 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { } } + protected class MockAppSearchManager implements IAppSearchManager { + + protected Map<String, List<PackageIdentifier>> mSchemasPackageAccessible = + new ArrayMap<>(1); + + @Override + public void setSchema(String packageName, String databaseName, List<Bundle> schemaBundles, + List<String> schemasNotPlatformSurfaceable, + Map<String, List<Bundle>> schemasPackageAccessibleBundles, boolean forceOverride, + int userId, IAppSearchResultCallback callback) throws RemoteException { + for (Map.Entry<String, List<Bundle>> entry : + schemasPackageAccessibleBundles.entrySet()) { + final String key = entry.getKey(); + final List<PackageIdentifier> packageIdentifiers; + if (!mSchemasPackageAccessible.containsKey(key)) { + packageIdentifiers = new ArrayList<>(entry.getValue().size()); + mSchemasPackageAccessible.put(key, packageIdentifiers); + } else { + packageIdentifiers = mSchemasPackageAccessible.get(key); + } + for (int i = 0; i < entry.getValue().size(); i++) { + packageIdentifiers.add(new PackageIdentifier(entry.getValue().get(i))); + } + } + callback.onResult(AppSearchResult.newSuccessfulResult(null)); + } + + @Override + public void getSchema(String packageName, String databaseName, int userId, + IAppSearchResultCallback callback) throws RemoteException { + ignore(callback); + } + + @Override + public void putDocuments(String packageName, String databaseName, + List<Bundle> documentBundles, int userId, IAppSearchBatchResultCallback callback) + throws RemoteException { + ignore(callback); + } + + @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); + } + + @Override + public void query(String packageName, String databaseName, String queryExpression, + Bundle searchSpecBundle, int userId, IAppSearchResultCallback callback) + throws RemoteException { + ignore(callback); + } + + @Override + public void globalQuery(String packageName, String queryExpression, Bundle searchSpecBundle, + int userId, IAppSearchResultCallback callback) throws RemoteException { + ignore(callback); + } + + @Override + public void getNextPage(long nextPageToken, int userId, IAppSearchResultCallback callback) + throws RemoteException { + ignore(callback); + } + + @Override + public void invalidateNextPageToken(long nextPageToken, int userId) throws RemoteException { + + } + + @Override + public void reportUsage(String packageName, String databaseName, String namespace, + String uri, long usageTimeMillis, int userId, IAppSearchResultCallback callback) + throws RemoteException { + ignore(callback); + } + + @Override + public void removeByUri(String packageName, String databaseName, String namespace, + List<String> uris, int userId, IAppSearchBatchResultCallback callback) + throws RemoteException { + ignore(callback); + } + + @Override + public void removeByQuery(String packageName, String databaseName, String queryExpression, + Bundle searchSpecBundle, int userId, IAppSearchResultCallback callback) + throws RemoteException { + ignore(callback); + } + + @Override + public void persistToDisk(int userId) throws RemoteException { + + } + + @Override + public void initialize(int userId, IAppSearchResultCallback callback) + throws RemoteException { + ignore(callback); + } + + @Override + public IBinder asBinder() { + return null; + } + + private void ignore(IAppSearchResultCallback callback) throws RemoteException { + callback.onResult(AppSearchResult.newSuccessfulResult(null)); + } + + private void ignore(IAppSearchBatchResultCallback callback) throws RemoteException { + callback.onResult(new AppSearchBatchResult.Builder().build()); + } + } + public static class ShortcutActivity extends Activity { } @@ -652,6 +784,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { protected PackageManagerInternal mMockPackageManagerInternal; protected UserManager mMockUserManager; protected DevicePolicyManager mMockDevicePolicyManager; + protected MockAppSearchManager mMockAppSearchManager; protected UserManagerInternal mMockUserManagerInternal; protected UsageStatsManagerInternal mMockUsageStatsManagerInternal; protected ActivityManagerInternal mMockActivityManagerInternal; @@ -801,6 +934,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { mMockPackageManagerInternal = mock(PackageManagerInternal.class); mMockUserManager = mock(UserManager.class); mMockDevicePolicyManager = mock(DevicePolicyManager.class); + mMockAppSearchManager = new MockAppSearchManager(); mMockUserManagerInternal = mock(UserManagerInternal.class); mMockUsageStatsManagerInternal = mock(UsageStatsManagerInternal.class); mMockActivityManagerInternal = mock(ActivityManagerInternal.class); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java new file mode 100644 index 000000000000..b17085ee0317 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest12.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.app.appsearch.PackageIdentifier; +import android.content.pm.AppSearchShortcutInfo; + +import java.util.Random; + +/** + * Tests for {@link android.app.appsearch.AppSearchManager} and relevant APIs in ShortcutManager. + * + atest -c com.android.server.pm.ShortcutManagerTest12 + */ +public class ShortcutManagerTest12 extends BaseShortcutManagerTest { + + public void testUpdateShortcutVisibility_updatesShortcutSchema() { + + final byte[] cert = new byte[20]; + new Random().nextBytes(cert); + + runWithCaller(CALLING_PACKAGE_1, USER_0, () -> { + mManager.updateShortcutVisibility(CALLING_PACKAGE_2, cert, true); + assertTrue(mMockAppSearchManager.mSchemasPackageAccessible.containsKey( + AppSearchShortcutInfo.SCHEMA_TYPE)); + assertTrue(mMockAppSearchManager.mSchemasPackageAccessible.get( + AppSearchShortcutInfo.SCHEMA_TYPE).get(0).equals( + new PackageIdentifier(CALLING_PACKAGE_2, cert))); + }); + } +} |