Merge "Shortcut integration with AppSearch (Part 1)" into sc-dev

This commit is contained in:
Pinyao Ting
2021-02-26 19:25:30 +00:00
committed by Android (Google) Code Review
8 changed files with 390 additions and 0 deletions

View File

@@ -12898,6 +12898,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

View File

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

View File

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

View File

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

View File

@@ -2149,6 +2149,15 @@ public class ShortcutService extends IShortcutService.Stub {
verifyStates();
}
@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) {

View File

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

View File

@@ -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.
@@ -158,6 +168,11 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
throw new UnsupportedOperationException("Couldn't find system service: " + name);
}
@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);

View File

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