diff --git a/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java b/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java
index 24d64a04affda..64dc972d301c0 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * 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.
@@ -14,9 +14,13 @@
* limitations under the License.
*/
+// TODO(b/169883602): This is purposely a different package from the path so that it can access
+// AppSearchImpl's methods without having to make them public. This should be moved into a proper
+// package once AppSearchImpl-VisibilityStore's dependencies are refactored.
package com.android.server.appsearch.external.localstorage;
import android.annotation.NonNull;
+import android.annotation.UserIdInt;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
@@ -25,10 +29,10 @@ import android.app.appsearch.exceptions.AppSearchException;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Process;
+import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
-
-import androidx.annotation.RequiresApi;
+import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -56,8 +60,16 @@ import java.util.Set;
*
*
NOTE: This class holds an instance of AppSearchImpl and AppSearchImpl holds an instance of
* this class. Take care to not cause any circular dependencies.
+ *
+ * @hide
*/
-class VisibilityStore {
+public class VisibilityStore {
+
+ private static final String TAG = "AppSearchVisibilityStore";
+
+ /** No-op user id that won't have any visibility settings. */
+ public static final int NO_OP_USER_ID = -1;
+
/** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
private static final String VISIBILITY_TYPE = "VisibilityType";
@@ -124,8 +136,8 @@ class VisibilityStore {
.build();
/**
- * These cannot have any of the special characters used by AppSearchImpl (e.g. {@link
- * AppSearchImpl#PACKAGE_DELIMITER} or {@link AppSearchImpl#DATABASE_DELIMITER}.
+ * These cannot have any of the special characters used by AppSearchImpl (e.g. {@code
+ * AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}.
*/
static final String PACKAGE_NAME = "VS#Pkg";
@@ -149,11 +161,15 @@ class VisibilityStore {
private final AppSearchImpl mAppSearchImpl;
+ // Context of the system service.
private final Context mContext;
+ // User ID of the caller who we're checking visibility settings for.
+ private final int mUserId;
+
// UID of the package that has platform-query privileges, i.e. can query for all
// platform-surfaceable content.
- private int mGlobalQuerierPackageUid;
+ private int mGlobalQuerierUid;
/**
* Maps prefixes to the set of schemas that are platform-hidden within that prefix. All schemas
@@ -180,20 +196,15 @@ class VisibilityStore {
*
* @param appSearchImpl AppSearchImpl instance
*/
- VisibilityStore(
+ public VisibilityStore(
@NonNull AppSearchImpl appSearchImpl,
@NonNull Context context,
+ @UserIdInt int userId,
@NonNull String globalQuerierPackage) {
mAppSearchImpl = appSearchImpl;
mContext = context;
- mGlobalQuerierPackageUid = Process.INVALID_UID;
-
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
- // This should always pass since we should only allow platform access on S+ (the first
- // version that AppSearch is offered on).
- mGlobalQuerierPackageUid =
- Api24Impl.getGlobalQuerierPackageUid(context, globalQuerierPackage);
- }
+ mUserId = userId;
+ mGlobalQuerierUid = getGlobalQuerierUid(globalQuerierPackage);
}
/**
@@ -357,7 +368,9 @@ class VisibilityStore {
Preconditions.checkNotNull(prefix);
Preconditions.checkNotNull(prefixedSchema);
- if (callerUid == mGlobalQuerierPackageUid
+ // We compare appIds here rather than direct uids because the package's uid may change based
+ // on the user that's running.
+ if (UserHandle.isSameApp(mGlobalQuerierUid, callerUid)
&& isSchemaPlatformSurfaceable(prefix, prefixedSchema)) {
return true;
}
@@ -414,26 +427,21 @@ class VisibilityStore {
return false;
}
- if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) {
- // PackageManager.hasSigningCertificate is only available on P+
- // This should never fail since we should only allow package access on S+ (the first
- // version that AppSearch is offered on). But just in case, default to no package
- // access.
- return false;
- }
-
for (PackageIdentifier packageIdentifier : packageIdentifiers) {
// Check that the caller uid matches this allowlisted PackageIdentifier.
- if (Api24Impl.getPackageUid(mContext, packageIdentifier.getPackageName())
- != callerUid) {
+ // TODO(b/169883602): Consider caching the UIDs of packages. Looking this up in the
+ // package manager could be costly. We would also need to update the cache on
+ // package-removals.
+ if (getPackageUidAsUser(packageIdentifier.getPackageName()) != callerUid) {
continue;
}
// Check that the package also has the matching certificate
- if (Api28Impl.hasSigningCertificate(
- mContext,
- packageIdentifier.getPackageName(),
- packageIdentifier.getSha256Certificate())) {
+ if (mContext.getPackageManager()
+ .hasSigningCertificate(
+ packageIdentifier.getPackageName(),
+ packageIdentifier.getSha256Certificate(),
+ PackageManager.CERT_INPUT_SHA256)) {
// The caller has the right package name and right certificate!
return true;
}
@@ -448,7 +456,7 @@ class VisibilityStore {
*
*
{@link #initialize()} must be called after this.
*/
- void handleReset() {
+ public void handleReset() {
mNotPlatformSurfaceableMap.clear();
mPackageAccessibleMap.clear();
}
@@ -464,83 +472,40 @@ class VisibilityStore {
}
/**
- * Wrapper class around API 24 methods.
- *
- *
Even though wrapping a call to a method from an API above minSdk inside an SDK_INT check
- * makes it runtime safe, it is not optimal. When ART tries to optimize a class, it will do so
- * regardless of the execution path, and will fail if it tries to resolve a method at a higher
- * API if that method is being referenced somewhere in the class, even if that method would
- * never be called at runtime due to the SDK_INT check. ART will however only try to optimize a
- * class the first time it's referenced at runtime, this means if we wrap our above minSdk
- * method calls inside classes that are only referenced at runtime at the appropriate API level,
- * then we guarantee the ability to resolve all the methods.
+ * Finds the uid of the {@code globalQuerierPackage}. {@code globalQuerierPackage} must be a
+ * pre-installed, system app. Returns {@link Process#INVALID_UID} if unable to find the UID.
*/
- @RequiresApi(24)
- private static class Api24Impl {
- private Api24Impl() {}
-
- /**
- * Finds the UID of the {@code globalQuerierPackage}. {@code globalQuerierPackage} must be a
- * pre-installed, system app. Returns {@link Process#INVALID_UID} if unable to find the UID.
- */
- static int getGlobalQuerierPackageUid(
- @NonNull Context context, @NonNull String globalQuerierPackage) {
- try {
- // TODO(b/169883602): In framework, this should be UserHandle.isSameApp or
- // packageManager.getPackageUidAsUser().
- int flags =
- PackageManager.MATCH_DISABLED_COMPONENTS
- | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
- | PackageManager.MATCH_SYSTEM_ONLY;
- return context.getPackageManager().getPackageUid(globalQuerierPackage, flags);
- } catch (PackageManager.NameNotFoundException e) {
- // Global querier doesn't exist.
- }
- return Process.INVALID_UID;
- }
-
- /**
- * Finds the UID of the {@code packageName}. Returns {@link Process#INVALID_UID} if unable
- * to find the UID.
- */
- static int getPackageUid(@NonNull Context context, @NonNull String packageName) {
- try {
- // TODO(b/169883602): In framework, this should be UserHandle.isSameApp or
- // packageManager.getPackageUidAsUser().
- return context.getPackageManager().getPackageUid(packageName, /*flags=*/ 0);
- } catch (PackageManager.NameNotFoundException e) {
- // Global querier doesn't exist.
- }
- return Process.INVALID_UID;
+ private int getGlobalQuerierUid(@NonNull String globalQuerierPackage) {
+ try {
+ int flags =
+ PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ | PackageManager.MATCH_SYSTEM_ONLY;
+ // It doesn't matter that we're using the caller's userId here. We'll eventually check
+ // that the two uids in question belong to the same appId.
+ return mContext.getPackageManager()
+ .getPackageUidAsUser(globalQuerierPackage, flags, mUserId);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Global querier doesn't exist.
+ Log.i(
+ TAG,
+ "AppSearch global querier package not found on device: '"
+ + globalQuerierPackage
+ + "'");
}
+ return Process.INVALID_UID;
}
/**
- * Wrapper class around API 28 methods.
- *
- *
Even though wrapping a call to a method from an API above minSdk inside an SDK_INT check
- * makes it runtime safe, it is not optimal. When ART tries to optimize a class, it will do so
- * regardless of the execution path, and will fail if it tries to resolve a method at a higher
- * API if that method is being referenced somewhere in the class, even if that method would
- * never be called at runtime due to the SDK_INT check. ART will however only try to optimize a
- * class the first time it's referenced at runtime, this means if we wrap our above minSdk
- * method calls inside classes that are only referenced at runtime at the appropriate API level,
- * then we guarantee the ability to resolve all the methods.
+ * Finds the UID of the {@code packageName}. Returns {@link Process#INVALID_UID} if unable to
+ * find the UID.
*/
- @RequiresApi(28)
- private static class Api28Impl {
- private Api28Impl() {}
-
- /**
- * Returns whether the {@code packageName} has been signed with {@code sha256Certificate}.
- */
- static boolean hasSigningCertificate(
- @NonNull Context context,
- @NonNull String packageName,
- @NonNull byte[] sha256Certificate) {
- return context.getPackageManager()
- .hasSigningCertificate(
- packageName, sha256Certificate, PackageManager.CERT_INPUT_SHA256);
+ private int getPackageUidAsUser(@NonNull String packageName) {
+ try {
+ return mContext.getPackageManager().getPackageUidAsUser(packageName, mUserId);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Package doesn't exist, continue
}
+ return Process.INVALID_UID;
}
}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
index b1d0ab422e58f..651ccd9fd3b83 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
@@ -192,7 +192,8 @@ public final class AppSearchImpl {
mIcingSearchEngineLocked = new IcingSearchEngine(options);
mVisibilityStoreLocked =
- new VisibilityStore(this, context, globalQuerierPackage);
+ new VisibilityStore(
+ this, context, userId, globalQuerierPackage);
InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize();
SchemaProto schemaProto;
diff --git a/services/tests/servicestests/src/com/android/server/appsearch/AppSearchImplPlatformTest.java b/services/tests/servicestests/src/com/android/server/appsearch/AppSearchImplPlatformTest.java
new file mode 100644
index 0000000000000..b5f49124483eb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appsearch/AppSearchImplPlatformTest.java
@@ -0,0 +1,426 @@
+/*
+ * 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.
+ */
+
+// TODO(b/169883602): This is purposely a different package from the path so that it can access
+// AppSearchImpl's methods without having to make them public. This should be replaced by proper
+// global query integration tests that can test AppSearchImpl-VisibilityStore integration logic.
+package com.android.server.appsearch.external.localstorage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.PackageIdentifier;
+import android.app.appsearch.SearchResultPage;
+import android.app.appsearch.SearchSpec;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.util.Collections;
+import java.util.List;
+
+/** This tests AppSearchImpl when it's running with a platform-backed VisibilityStore. */
+public class AppSearchImplPlatformTest {
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private MockPackageManager mMockPackageManager = new MockPackageManager();
+ private Context mContext;
+ private AppSearchImpl mAppSearchImpl;
+ private int mGlobalQuerierUid;
+
+ @Before
+ public void setUp() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ mContext =
+ new ContextWrapper(context) {
+ @Override
+ public PackageManager getPackageManager() {
+ return mMockPackageManager.getMockPackageManager();
+ }
+ };
+
+ // Give ourselves global query permissions
+ mAppSearchImpl =
+ AppSearchImpl.create(
+ mTemporaryFolder.newFolder(),
+ mContext,
+ mContext.getUserId(),
+ mContext.getPackageName());
+ mGlobalQuerierUid =
+ mContext.getPackageManager().getPackageUid(mContext.getPackageName(), /*flags=*/ 0);
+ }
+ /**
+ * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term
+ * test until we have official support for multiple-apps indexing at once.
+ */
+ @Test
+ public void testGlobalQueryWithMultiplePackages_noPackageFilters() throws Exception {
+ // Insert package1 schema
+ List schema1 =
+ ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+ mAppSearchImpl.setSchema(
+ "package1",
+ "database1",
+ schema1,
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+
+ // Insert package2 schema
+ List schema2 =
+ ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
+ mAppSearchImpl.setSchema(
+ "package2",
+ "database2",
+ schema2,
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+
+ // Insert package1 document
+ GenericDocument document1 =
+ new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build();
+ mAppSearchImpl.putDocument("package1", "database1", document1);
+
+ // Insert package2 document
+ GenericDocument document2 =
+ new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build();
+ mAppSearchImpl.putDocument("package2", "database2", document2);
+
+ // No query filters specified, global query can retrieve all documents.
+ SearchSpec searchSpec =
+ new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).build();
+ SearchResultPage searchResultPage =
+ mAppSearchImpl.globalQuery(
+ "", searchSpec, mContext.getPackageName(), mGlobalQuerierUid);
+ assertThat(searchResultPage.getResults()).hasSize(2);
+
+ // Document2 will be first since it got indexed later and has a "better", aka more recent
+ // score.
+ assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document2);
+ assertThat(searchResultPage.getResults().get(1).getDocument()).isEqualTo(document1);
+ }
+
+ /**
+ * TODO(b/169883602): This should be an integration test at the cts-level. This is a short-term
+ * test until we have official support for multiple-apps indexing at once.
+ */
+ @Test
+ public void testGlobalQueryWithMultiplePackages_withPackageFilters() throws Exception {
+ // Insert package1 schema
+ List schema1 =
+ ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+ mAppSearchImpl.setSchema(
+ "package1",
+ "database1",
+ schema1,
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+
+ // Insert package2 schema
+ List schema2 =
+ ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
+ mAppSearchImpl.setSchema(
+ "package2",
+ "database2",
+ schema2,
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+
+ // Insert package1 document
+ GenericDocument document1 =
+ new GenericDocument.Builder<>("uri", "schema1").setNamespace("namespace").build();
+ mAppSearchImpl.putDocument("package1", "database1", document1);
+
+ // Insert package2 document
+ GenericDocument document2 =
+ new GenericDocument.Builder<>("uri", "schema2").setNamespace("namespace").build();
+ mAppSearchImpl.putDocument("package2", "database2", document2);
+
+ // "package1" filter specified
+ SearchSpec searchSpec =
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .addFilterPackageNames("package1")
+ .build();
+ SearchResultPage searchResultPage =
+ mAppSearchImpl.globalQuery(
+ "", searchSpec, mContext.getPackageName(), mGlobalQuerierUid);
+ assertThat(searchResultPage.getResults()).hasSize(1);
+ assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document1);
+
+ // "package2" filter specified
+ searchSpec =
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .addFilterPackageNames("package2")
+ .build();
+ searchResultPage =
+ mAppSearchImpl.globalQuery(
+ "", searchSpec, mContext.getPackageName(), mGlobalQuerierUid);
+ assertThat(searchResultPage.getResults()).hasSize(1);
+ assertThat(searchResultPage.getResults().get(0).getDocument()).isEqualTo(document2);
+ }
+
+ @Test
+ public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[] {10};
+ int uidFoo = 1;
+
+ // Make sure foo package will pass package manager checks.
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo);
+ mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo);
+
+ // Set schema1
+ String prefix = AppSearchImpl.createPrefix("package", "database");
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("schema1").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "schema1",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))),
+ /*forceOverride=*/ false);
+
+ // "schema1" is platform hidden now and package visible to package1
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "schema1", mGlobalQuerierUid))
+ .isFalse();
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo))
+ .isTrue();
+
+ // Add a new schema, and include the already-existing "schema1"
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("schema1").build(),
+ new AppSearchSchema.Builder("schema2").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "schema1",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))),
+ /*forceOverride=*/ false);
+
+ // Check that "schema1" still has the same visibility settings
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "schema1", mGlobalQuerierUid))
+ .isFalse();
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo))
+ .isTrue();
+
+ // "schema2" has default visibility settings
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "schema2", mGlobalQuerierUid))
+ .isTrue();
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(prefix, prefix + "schema2", uidFoo))
+ .isFalse();
+ }
+
+ @Test
+ public void testRemoveSchema_removedFromVisibilityStore() throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[] {10};
+ int uidFoo = 1;
+
+ // Make sure foo package will pass package manager checks.
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo);
+ mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo);
+
+ String prefix = AppSearchImpl.createPrefix("package", "database");
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("schema1").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "schema1",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))),
+ /*forceOverride=*/ false);
+
+ // "schema1" is platform hidden now and package accessible
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "schema1", mGlobalQuerierUid))
+ .isFalse();
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo))
+ .isTrue();
+
+ // Remove "schema1" by force overriding
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ /*schemas=*/ Collections.emptyList(),
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ true);
+
+ // Check that "schema1" is no longer considered platform hidden or package accessible
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "schema1", mGlobalQuerierUid))
+ .isTrue();
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo))
+ .isFalse();
+
+ // Add "schema1" back, it gets default visibility settings which means it's not platform
+ // hidden and not package accessible
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("schema1").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "schema1", mGlobalQuerierUid))
+ .isTrue();
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(prefix, prefix + "schema1", uidFoo))
+ .isFalse();
+ }
+
+ @Test
+ public void testSetSchema_defaultPlatformVisible() throws Exception {
+ String prefix = AppSearchImpl.createPrefix("package", "database");
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("Schema").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "Schema", mGlobalQuerierUid))
+ .isTrue();
+ }
+
+ @Test
+ public void testSetSchema_platformHidden() throws Exception {
+ String prefix = AppSearchImpl.createPrefix("package", "database");
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("Schema").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("Schema"),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "Schema", mGlobalQuerierUid))
+ .isFalse();
+ }
+
+ @Test
+ public void testSetSchema_defaultNotPackageAccessible() throws Exception {
+ String prefix = AppSearchImpl.createPrefix("package", "database");
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("Schema").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap(),
+ /*forceOverride=*/ false);
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(
+ prefix, prefix + "Schema", /*callerUid=*/ 42))
+ .isFalse();
+ }
+
+ @Test
+ public void testSetSchema_packageAccessible() throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[] {10};
+ int uidFoo = 1;
+
+ // Make sure foo package will pass package manager checks.
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo);
+ mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo);
+
+ String prefix = AppSearchImpl.createPrefix("package", "database");
+ mAppSearchImpl.setSchema(
+ "package",
+ "database",
+ Collections.singletonList(new AppSearchSchema.Builder("Schema").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "Schema",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))),
+ /*forceOverride=*/ false);
+ assertThat(
+ mAppSearchImpl
+ .getVisibilityStoreLocked()
+ .isSchemaSearchableByCaller(prefix, prefix + "Schema", uidFoo))
+ .isTrue();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/appsearch/MockPackageManager.java b/services/tests/servicestests/src/com/android/server/appsearch/MockPackageManager.java
new file mode 100644
index 0000000000000..459fc53dcc08f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appsearch/MockPackageManager.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+// TODO(b/169883602): This is purposely a different package from the path so that AppSearchImplTest
+// can use it without an extra import. This should be moved into a proper package once
+// AppSearchImpl-VisibilityStore's dependencies are refactored.
+package com.android.server.appsearch.external.localstorage;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.pm.PackageManager;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Mock to help test package name, UID, and certificate verification. */
+public class MockPackageManager {
+
+ @Mock private PackageManager mMockPackageManager;
+
+ public MockPackageManager() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @NonNull
+ public PackageManager getMockPackageManager() {
+ return mMockPackageManager;
+ }
+
+ /** Mock a NameNotFoundException if the package name isn't installed. */
+ public void mockThrowsNameNotFoundException(String packageName) {
+ try {
+ when(mMockPackageManager.getPackageUidAsUser(eq(packageName), /*userId=*/ anyInt()))
+ .thenThrow(new PackageManager.NameNotFoundException());
+ when(mMockPackageManager.getPackageUidAsUser(
+ eq(packageName), /*flags=*/ anyInt(), /*userId=*/ anyInt()))
+ .thenThrow(new PackageManager.NameNotFoundException());
+ } catch (PackageManager.NameNotFoundException e) {
+ // Shouldn't ever happen since we're mocking the exception
+ e.printStackTrace();
+ }
+ }
+
+ /** Mocks that {@code uid} contains the {@code packageName} */
+ public void mockGetPackageUidAsUser(String packageName, @UserIdInt int callerUserId, int uid) {
+ try {
+ when(mMockPackageManager.getPackageUidAsUser(eq(packageName), eq(callerUserId)))
+ .thenReturn(uid);
+ when(mMockPackageManager.getPackageUidAsUser(
+ eq(packageName), /*flags=*/ anyInt(), eq(callerUserId)))
+ .thenReturn(uid);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Shouldn't ever happen since we're mocking the method.
+ e.printStackTrace();
+ }
+ }
+
+ /** Mocks that {@code packageName} has been signed with {@code sha256Cert}. */
+ public void mockAddSigningCertificate(String packageName, byte[] sha256Cert) {
+ when(mMockPackageManager.hasSigningCertificate(
+ packageName, sha256Cert, PackageManager.CERT_INPUT_SHA256))
+ .thenReturn(true);
+ }
+
+ /** Mocks that {@code packageName} has NOT been signed with {@code sha256Cert}. */
+ public void mockRemoveSigningCertificate(String packageName, byte[] sha256Cert) {
+ when(mMockPackageManager.hasSigningCertificate(
+ packageName, sha256Cert, PackageManager.CERT_INPUT_SHA256))
+ .thenReturn(false);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/appsearch/VisibilityStoreTest.java b/services/tests/servicestests/src/com/android/server/appsearch/VisibilityStoreTest.java
new file mode 100644
index 0000000000000..8d35ebe8e2e11
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appsearch/VisibilityStoreTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.
+ */
+
+// TODO(b/169883602): This is purposely a different package from the path so that it can access
+// AppSearchImpl and VisibilityStore methods without having to make methods public. This should be
+// moved into a proper package once AppSearchImpl-VisibilityStore's dependencies are refactored.
+package com.android.server.appsearch.external.localstorage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.PackageIdentifier;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.util.Collections;
+
+public class VisibilityStoreTest {
+
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private MockPackageManager mMockPackageManager = new MockPackageManager();
+ private Context mContext;
+ private AppSearchImpl mAppSearchImpl;
+ private VisibilityStore mVisibilityStore;
+ private int mGlobalQuerierUid;
+
+ @Before
+ public void setUp() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ mContext =
+ new ContextWrapper(context) {
+ @Override
+ public PackageManager getPackageManager() {
+ return mMockPackageManager.getMockPackageManager();
+ }
+ };
+
+ // Give ourselves global query permissions
+ mAppSearchImpl =
+ AppSearchImpl.create(
+ mTemporaryFolder.newFolder(),
+ mContext,
+ mContext.getUserId(),
+ /*globalQuerierPackage=*/ mContext.getPackageName());
+ mGlobalQuerierUid =
+ mContext.getPackageManager().getPackageUid(mContext.getPackageName(), /*flags=*/ 0);
+
+ mVisibilityStore = mAppSearchImpl.getVisibilityStoreLocked();
+ }
+
+ /**
+ * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved.
+ */
+ @Test
+ public void testValidPackageName() {
+ assertThat(VisibilityStore.PACKAGE_NAME)
+ .doesNotContain(
+ "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences
+ assertThat(VisibilityStore.PACKAGE_NAME)
+ .doesNotContain(
+ ""
+ + AppSearchImpl
+ .DATABASE_DELIMITER); // Convert the chars to CharSequences
+ }
+
+ /**
+ * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved.
+ */
+ @Test
+ public void testValidDatabaseName() {
+ assertThat(VisibilityStore.DATABASE_NAME)
+ .doesNotContain(
+ "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences
+ assertThat(VisibilityStore.DATABASE_NAME)
+ .doesNotContain(
+ ""
+ + AppSearchImpl
+ .DATABASE_DELIMITER); // Convert the chars to CharSequences
+ }
+
+ @Test
+ public void testSetVisibility_platformSurfaceable() throws Exception {
+ mVisibilityStore.setVisibility(
+ "prefix",
+ /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of(
+ "prefix/schema1", "prefix/schema2"),
+ /*schemasPackageAccessible=*/ Collections.emptyMap());
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema1", mGlobalQuerierUid))
+ .isFalse();
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema2", mGlobalQuerierUid))
+ .isFalse();
+
+ // New .setVisibility() call completely overrides previous visibility settings. So
+ // "schema2" isn't preserved.
+ mVisibilityStore.setVisibility(
+ "prefix",
+ /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of(
+ "prefix/schema1", "prefix/schema3"),
+ /*schemasPackageAccessible=*/ Collections.emptyMap());
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema1", mGlobalQuerierUid))
+ .isFalse();
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema2", mGlobalQuerierUid))
+ .isTrue();
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema3", mGlobalQuerierUid))
+ .isFalse();
+
+ // Everything defaults to visible again.
+ mVisibilityStore.setVisibility(
+ "prefix",
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
+ /*schemasPackageAccessible=*/ Collections.emptyMap());
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema1", mGlobalQuerierUid))
+ .isTrue();
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema2", mGlobalQuerierUid))
+ .isTrue();
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schema3", mGlobalQuerierUid))
+ .isTrue();
+ }
+
+ @Test
+ public void testIsSchemaSearchableByCaller_platformQuerierHandlesNameNotFoundException()
+ throws Exception {
+ // Initialized the VisibilityStore with this context's package name as the global querier.
+ mMockPackageManager.mockThrowsNameNotFoundException(mContext.getPackageName());
+
+ // Create a new VisibilityStore instance since we look up the UID on initialization
+ AppSearchImpl appSearchImpl =
+ AppSearchImpl.create(
+ mTemporaryFolder.newFolder(),
+ mContext,
+ mContext.getUserId(),
+ /*globalQuerierPackage=*/ mContext.getPackageName());
+ VisibilityStore visibilityStore = appSearchImpl.getVisibilityStoreLocked();
+
+ // Use some arbitrary callerUid. If we can't find the global querier's uid though,
+ // nothing should be platform surfaceable.
+ assertThat(
+ visibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaFoo", /*callerUid=*/ 0))
+ .isFalse();
+ }
+
+ @Test
+ public void testSetVisibility_packageAccessible() throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[] {10};
+ int uidFoo = 1;
+
+ // Values for a "bar" client
+ String packageNameBar = "packageBar";
+ byte[] sha256CertBar = new byte[] {100};
+ int uidBar = 2;
+
+ // Can't be the same value as uidFoo nor uidBar
+ int uidNotFooOrBar = 3;
+
+ // By default, a schema isn't package accessible.
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaFoo", uidFoo))
+ .isFalse();
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaBar", uidBar))
+ .isFalse();
+
+ // Grant package access
+ mVisibilityStore.setVisibility(
+ "prefix",
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "prefix/schemaFoo",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo)),
+ "prefix/schemaBar",
+ ImmutableList.of(new PackageIdentifier(packageNameBar, sha256CertBar))));
+
+ // Should fail if PackageManager doesn't see that it has the proper certificate
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo);
+ mMockPackageManager.mockRemoveSigningCertificate(packageNameFoo, sha256CertFoo);
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaFoo", uidFoo))
+ .isFalse();
+
+ // Should fail if PackageManager doesn't think the package belongs to the uid
+ mMockPackageManager.mockGetPackageUidAsUser(
+ packageNameFoo, mContext.getUserId(), uidNotFooOrBar);
+ mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo);
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaFoo", uidFoo))
+ .isFalse();
+
+ // But if uid and certificate match, then we should have access
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo);
+ mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo);
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaFoo", uidFoo))
+ .isTrue();
+
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameBar, mContext.getUserId(), uidBar);
+ mMockPackageManager.mockAddSigningCertificate(packageNameBar, sha256CertBar);
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaBar", uidBar))
+ .isTrue();
+
+ // New .setVisibility() call completely overrides previous visibility settings. So
+ // "schemaBar" settings aren't preserved.
+ mVisibilityStore.setVisibility(
+ "prefix",
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "prefix/schemaFoo",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))));
+
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo);
+ mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo);
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaFoo", uidFoo))
+ .isTrue();
+
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameBar, mContext.getUserId(), uidBar);
+ mMockPackageManager.mockAddSigningCertificate(packageNameBar, sha256CertBar);
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaBar", uidBar))
+ .isFalse();
+ }
+
+ @Test
+ public void testIsSchemaSearchableByCaller_packageAccessibilityHandlesNameNotFoundException()
+ throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[] {10};
+ int uidFoo = 1;
+
+ // Pretend we can't find the Foo package.
+ mMockPackageManager.mockThrowsNameNotFoundException(packageNameFoo);
+
+ // Grant package access
+ mVisibilityStore.setVisibility(
+ "prefix",
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "prefix/schemaFoo",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))));
+
+ // If we can't verify the Foo package that has access, assume it doesn't have access.
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ "prefix", "prefix/schemaFoo", uidFoo))
+ .isFalse();
+ }
+
+ @Test
+ public void testEmptyPrefix() throws Exception {
+ // Values for a "foo" client
+ String packageNameFoo = "packageFoo";
+ byte[] sha256CertFoo = new byte[] {10};
+ int uidFoo = 1;
+
+ mVisibilityStore.setVisibility(
+ /*prefix=*/ "",
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
+ /*schemasPackageAccessible=*/ ImmutableMap.of(
+ "schema",
+ ImmutableList.of(new PackageIdentifier(packageNameFoo, sha256CertFoo))));
+
+ assertThat(
+ mVisibilityStore.isSchemaSearchableByCaller(
+ /*prefix=*/ "", "schema", mGlobalQuerierUid))
+ .isTrue();
+
+ mMockPackageManager.mockGetPackageUidAsUser(packageNameFoo, mContext.getUserId(), uidFoo);
+ mMockPackageManager.mockAddSigningCertificate(packageNameFoo, sha256CertFoo);
+ assertThat(mVisibilityStore.isSchemaSearchableByCaller(/*prefix=*/ "", "schema", uidFoo))
+ .isTrue();
+ }
+}