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
This commit is contained in:
Pinyao Ting
2020-11-16 17:37:58 -08:00
parent 5f573843ca
commit b41bc495ec
6 changed files with 669 additions and 427 deletions

View File

@@ -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> {

File diff suppressed because it is too large Load Diff

View File

@@ -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) {

View File

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

View File

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

View File

@@ -1385,6 +1385,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
mService.waitForBitmapSavesForTest();
assertWith(getCallerShortcuts())
.forShortcutWithId("s1", si -> {
Log.d("ShortcutManagerTest1", si.toString());
assertTrue(si.hasIconFile());
});
@@ -1702,8 +1703,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
// Because setDynamicShortcuts will update the timestamps when ranks are changing,
// we explicitly set timestamps here.
getCallerShortcut("s1").setTimestamp(5000);
getCallerShortcut("s2").setTimestamp(1000);
updateCallerShortcut("s1", si -> si.setTimestamp(5000));
updateCallerShortcut("s2", si -> si.setTimestamp(1000));
setCaller(CALLING_PACKAGE_2);
final ShortcutInfo s2_2 = makeShortcut("s2");
@@ -1713,9 +1714,9 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
makeComponent(ShortcutActivity.class));
assertTrue(mManager.setDynamicShortcuts(list(s2_2, s2_3, s2_4)));
getCallerShortcut("s2").setTimestamp(1500);
getCallerShortcut("s3").setTimestamp(3000);
getCallerShortcut("s4").setTimestamp(500);
updateCallerShortcut("s2", si -> si.setTimestamp(1500));
updateCallerShortcut("s3", si -> si.setTimestamp(3000));
updateCallerShortcut("s4", si -> si.setTimestamp(500));
setCaller(CALLING_PACKAGE_3);
final ShortcutInfo s3_2 = makeShortcutWithLocusId("s3", makeLocusId("l2"));
@@ -1723,7 +1724,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
assertTrue(mManager.setDynamicShortcuts(list(s3_2)));
getCallerShortcut("s3").setTimestamp(START_TIME + 5000);
updateCallerShortcut("s3", si -> si.setTimestamp(START_TIME + 5000));
setCaller(LAUNCHER_1);
@@ -7686,7 +7687,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
assertEquals("http://www/", si.getIntent().getData().toString());
assertEquals("foo/bar", si.getIntent().getType());
assertEquals(
new ComponentName("abc", ".xyz"), si.getIntent().getComponent());
new ComponentName("abc", "abc.xyz"), si.getIntent().getComponent());
assertEquals(set("cat1", "cat2"), si.getIntent().getCategories());
assertEquals("value1", si.getIntent().getStringExtra("key1"));