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:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user