Refactor VisibilityStore's docs and maps to separate objects.

This reduces the volume of code VisibilityStore has to manage.

Bug: 169883602
Test: VisibilityStoreTest
Change-Id: I422f1c5f35fd4ded49cbd102530c574092bb4a74
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 0f643c5..b7e2159 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/VisibilityStore.java
@@ -36,6 +36,10 @@
 import android.util.Log;
 
 import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
+import com.android.server.appsearch.visibilitystore.NotPlatformSurfaceableMap;
+import com.android.server.appsearch.visibilitystore.PackageAccessibleDocument;
+import com.android.server.appsearch.visibilitystore.PackageAccessibleMap;
+import com.android.server.appsearch.visibilitystore.VisibilityDocument;
 
 import com.google.android.icing.proto.PersistType;
 
@@ -74,74 +78,10 @@
     /** 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";
-
     /** Version for the visibility schema */
     private static final int SCHEMA_VERSION = 0;
 
     /**
-     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
-     */
-    private static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable";
-
-    /** Property that holds nested documents of package accessible schemas. */
-    private static final String PACKAGE_ACCESSIBLE_PROPERTY = "packageAccessible";
-
-    /** Schema type for nested documents that hold package accessible information. */
-    private static final String PACKAGE_ACCESSIBLE_TYPE = "PackageAccessibleType";
-
-    /** Property that holds the package name that can access a schema. */
-    private static final String PACKAGE_NAME_PROPERTY = "packageName";
-
-    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
-    private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
-
-    /** Property that holds the prefixed schema type that is accessible by some package. */
-    private static final String ACCESSIBLE_SCHEMA_PROPERTY = "accessibleSchema";
-
-    /** Schema for the VisibilityStore's documents. */
-    private static final AppSearchSchema VISIBILITY_SCHEMA =
-            new AppSearchSchema.Builder(VISIBILITY_TYPE)
-                    .addProperty(
-                            new AppSearchSchema.StringPropertyConfig.Builder(
-                                    NOT_PLATFORM_SURFACEABLE_PROPERTY)
-                                    .setCardinality(
-                                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                    .build())
-                    .addProperty(
-                            new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                    PACKAGE_ACCESSIBLE_PROPERTY, PACKAGE_ACCESSIBLE_TYPE)
-                                    .setCardinality(
-                                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                    .build())
-                    .build();
-
-    /**
-     * Schema for package accessible documents, these will be nested in a top-level visibility
-     * document.
-     */
-    private static final AppSearchSchema PACKAGE_ACCESSIBLE_SCHEMA =
-            new AppSearchSchema.Builder(PACKAGE_ACCESSIBLE_TYPE)
-                    .addProperty(
-                            new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)
-                                    .setCardinality(
-                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                    .build())
-                    .addProperty(
-                            new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)
-                                    .setCardinality(
-                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                    .build())
-                    .addProperty(
-                            new AppSearchSchema.StringPropertyConfig.Builder(
-                                            ACCESSIBLE_SCHEMA_PROPERTY)
-                                    .setCardinality(
-                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                    .build())
-                    .build();
-
-    /**
      * These cannot have any of the special characters used by AppSearchImpl (e.g. {@code
      * AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}.
      */
@@ -177,24 +117,12 @@
     // platform-surfaceable content.
     private int mGlobalQuerierUid;
 
-    /**
-     * Maps prefixes to the set of schemas that are platform-hidden within that prefix. All schemas
-     * in the map are prefixed.
-     *
-     * <p>Although the prefix key isn't used for lookup, it's helpful in ensuring that all previous
-     * visibility settings for a prefix are completely overridden by new visibility settings.
-     */
-    private final Map<String, Set<String>> mNotPlatformSurfaceableMap = new ArrayMap<>();
+    /** Stores the schemas that are platform-hidden. All values are prefixed. */
+    private final NotPlatformSurfaceableMap mNotPlatformSurfaceableMap =
+            new NotPlatformSurfaceableMap();
 
-    /**
-     * Maps prefixes to a an internal map. The internal map maps prefixed schemas to the set of
-     * PackageIdentifiers that have access to that schema.
-     *
-     * <p>Although the prefix key isn't used for lookup, it's helpful in ensuring that all previous
-     * visibility settings for a prefix are completely overridden by new visibility settings.
-     */
-    private final Map<String, Map<String, Set<PackageIdentifier>>> mPackageAccessibleMap =
-            new ArrayMap<>();
+    /** Stores the schemas that are package accessible. All values are prefixed. */
+    private final PackageAccessibleMap mPackageAccessibleMap = new PackageAccessibleMap();
 
     /**
      * Creates an uninitialized VisibilityStore object. Callers must also call {@link #initialize()}
@@ -228,9 +156,9 @@
         boolean hasVisibilityType = false;
         boolean hasPackageAccessibleType = false;
         for (AppSearchSchema schema : getSchemaResponse.getSchemas()) {
-            if (schema.getSchemaType().equals(VISIBILITY_TYPE)) {
+            if (schema.getSchemaType().equals(VisibilityDocument.SCHEMA_TYPE)) {
                 hasVisibilityType = true;
-            } else if (schema.getSchemaType().equals(PACKAGE_ACCESSIBLE_TYPE)) {
+            } else if (schema.getSchemaType().equals(PackageAccessibleDocument.SCHEMA_TYPE)) {
                 hasPackageAccessibleType = true;
             }
 
@@ -244,7 +172,7 @@
             mAppSearchImpl.setSchema(
                     PACKAGE_NAME,
                     DATABASE_NAME,
-                    Arrays.asList(VISIBILITY_SCHEMA, PACKAGE_ACCESSIBLE_SCHEMA),
+                    Arrays.asList(VisibilityDocument.SCHEMA, PackageAccessibleDocument.SCHEMA),
                     /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
                     /*schemasPackageAccessible=*/ Collections.emptyMap(),
                     /*forceOverride=*/ false,
@@ -261,40 +189,34 @@
 
             try {
                 // Note: We use the other clients' prefixed names as ids
-                GenericDocument document =
+                VisibilityDocument visibilityDocument = new VisibilityDocument(
                         mAppSearchImpl.getDocument(
                                 PACKAGE_NAME,
                                 DATABASE_NAME,
                                 NAMESPACE,
                                 /*id=*/ addIdPrefix(prefix),
-                                /*typePropertyPaths=*/ Collections.emptyMap());
+                                /*typePropertyPaths=*/ Collections.emptyMap()));
 
                 // Update platform visibility settings
                 String[] notPlatformSurfaceableSchemas =
-                        document.getPropertyStringArray(NOT_PLATFORM_SURFACEABLE_PROPERTY);
+                        visibilityDocument.getNotPlatformSurfaceableSchemas();
                 if (notPlatformSurfaceableSchemas != null) {
-                    mNotPlatformSurfaceableMap.put(
-                            prefix, new ArraySet<>(Arrays.asList(notPlatformSurfaceableSchemas)));
+                    mNotPlatformSurfaceableMap.setNotPlatformSurfaceable(
+                            prefix,
+                            new ArraySet<>(notPlatformSurfaceableSchemas));
                 }
 
                 // Update 3p package visibility settings
                 Map<String, Set<PackageIdentifier>> schemaToPackageIdentifierMap = new ArrayMap<>();
                 GenericDocument[] packageAccessibleDocuments =
-                        document.getPropertyDocumentArray(PACKAGE_ACCESSIBLE_PROPERTY);
+                        visibilityDocument.getPackageAccessibleSchemas();
                 if (packageAccessibleDocuments != null) {
                     for (int i = 0; i < packageAccessibleDocuments.length; i++) {
-                        String packageName =
-                                packageAccessibleDocuments[i].getPropertyString(
-                                        PACKAGE_NAME_PROPERTY);
-                        byte[] sha256Cert =
-                                packageAccessibleDocuments[i].getPropertyBytes(
-                                        SHA_256_CERT_PROPERTY);
+                        PackageAccessibleDocument packageAccessibleDocument =
+                                new PackageAccessibleDocument(packageAccessibleDocuments[i]);
                         PackageIdentifier packageIdentifier =
-                                new PackageIdentifier(packageName, sha256Cert);
-
-                        String prefixedSchema =
-                                packageAccessibleDocuments[i].getPropertyString(
-                                        ACCESSIBLE_SCHEMA_PROPERTY);
+                                packageAccessibleDocument.getPackageIdentifier();
+                        String prefixedSchema = packageAccessibleDocument.getAccessibleSchemaType();
                         Set<PackageIdentifier> packageIdentifiers =
                                 schemaToPackageIdentifierMap.get(prefixedSchema);
                         if (packageIdentifiers == null) {
@@ -304,7 +226,7 @@
                         schemaToPackageIdentifierMap.put(prefixedSchema, packageIdentifiers);
                     }
                 }
-                mPackageAccessibleMap.put(prefix, schemaToPackageIdentifierMap);
+                mPackageAccessibleMap.setPackageAccessible(prefix, schemaToPackageIdentifierMap);
             } catch (AppSearchException e) {
                 if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
                     // TODO(b/172068212): This indicates some desync error. We were expecting a
@@ -339,38 +261,30 @@
         Objects.requireNonNull(schemasPackageAccessible);
 
         // Persist the document
-        GenericDocument.Builder<?> visibilityDocument =
-                new GenericDocument.Builder<>(
-                        NAMESPACE, /*id=*/ addIdPrefix(prefix), VISIBILITY_TYPE);
+        VisibilityDocument.Builder visibilityDocument =
+                new VisibilityDocument.Builder(NAMESPACE, /*id=*/ addIdPrefix(prefix));
         if (!schemasNotPlatformSurfaceable.isEmpty()) {
-            visibilityDocument.setPropertyString(
-                    NOT_PLATFORM_SURFACEABLE_PROPERTY,
+            visibilityDocument.setSchemasNotPlatformSurfaceable(
                     schemasNotPlatformSurfaceable.toArray(new String[0]));
         }
 
         Map<String, Set<PackageIdentifier>> schemaToPackageIdentifierMap = new ArrayMap<>();
-        List<GenericDocument> packageAccessibleDocuments = new ArrayList<>();
+        List<PackageAccessibleDocument> packageAccessibleDocuments = new ArrayList<>();
         for (Map.Entry<String, List<PackageIdentifier>> entry :
                 schemasPackageAccessible.entrySet()) {
             for (int i = 0; i < entry.getValue().size(); i++) {
-                GenericDocument packageAccessibleDocument = new GenericDocument.Builder<>(
-                        NAMESPACE, /*id=*/ "", PACKAGE_ACCESSIBLE_TYPE)
-                        .setPropertyString(
-                                PACKAGE_NAME_PROPERTY,
-                                entry.getValue().get(i).getPackageName())
-                        .setPropertyBytes(
-                                SHA_256_CERT_PROPERTY,
-                                entry.getValue().get(i).getSha256Certificate())
-                        .setPropertyString(ACCESSIBLE_SCHEMA_PROPERTY, entry.getKey())
-                        .build();
+                PackageAccessibleDocument packageAccessibleDocument =
+                        new PackageAccessibleDocument.Builder(NAMESPACE, /*id=*/ "")
+                                .setAccessibleSchemaType(entry.getKey())
+                                .setPackageIdentifier(entry.getValue().get(i))
+                                .build();
                 packageAccessibleDocuments.add(packageAccessibleDocument);
             }
             schemaToPackageIdentifierMap.put(entry.getKey(), new ArraySet<>(entry.getValue()));
         }
         if (!packageAccessibleDocuments.isEmpty()) {
-            visibilityDocument.setPropertyDocument(
-                    PACKAGE_ACCESSIBLE_PROPERTY,
-                    packageAccessibleDocuments.toArray(new GenericDocument[0]));
+            visibilityDocument.setPackageAccessibleSchemas(
+                    packageAccessibleDocuments.toArray(new PackageAccessibleDocument[0]));
         }
 
         mAppSearchImpl.putDocument(
@@ -379,8 +293,8 @@
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         // Update derived data structures.
-        mNotPlatformSurfaceableMap.put(prefix, schemasNotPlatformSurfaceable);
-        mPackageAccessibleMap.put(prefix, schemaToPackageIdentifierMap);
+        mNotPlatformSurfaceableMap.setNotPlatformSurfaceable(prefix, schemasNotPlatformSurfaceable);
+        mPackageAccessibleMap.setPackageAccessible(prefix, schemaToPackageIdentifierMap);
     }
 
     /** Checks whether {@code prefixedSchema} can be searched over by the {@code callerUid}. */
@@ -389,10 +303,14 @@
         Objects.requireNonNull(prefix);
         Objects.requireNonNull(prefixedSchema);
 
+        if (prefix.equals(VISIBILITY_STORE_PREFIX)) {
+            return false; // VisibilityStore schemas are for internal bookkeeping.
+        }
+
         // 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)) {
+                && mNotPlatformSurfaceableMap.isSchemaPlatformSurfaceable(prefix, prefixedSchema)) {
             return true;
         }
 
@@ -401,29 +319,6 @@
     }
 
     /**
-     * Returns whether the caller has platform query privileges, and if so, that the schema is
-     * surfaceable on the platform.
-     */
-    private boolean isSchemaPlatformSurfaceable(
-            @NonNull String prefix, @NonNull String prefixedSchema) {
-        if (prefix.equals(VISIBILITY_STORE_PREFIX)) {
-            // VisibilityStore schemas are for internal bookkeeping.
-            return false;
-        }
-
-        Set<String> notPlatformSurfaceableSchemas = mNotPlatformSurfaceableMap.get(prefix);
-        if (notPlatformSurfaceableSchemas == null) {
-            // No schemas were opted out of being platform-surfaced. So by default, it can be
-            // surfaced.
-            return true;
-        }
-
-        // Some schemas were opted out of being platform-surfaced. As long as this schema
-        // isn't one of those opt-outs, it's surfaceable.
-        return !notPlatformSurfaceableSchemas.contains(prefixedSchema);
-    }
-
-    /**
      * Returns whether the schema is accessible by the {@code callerUid}. Checks that the callerUid
      * has one of the allowed PackageIdentifier's package. And if so, that the package also has the
      * matching certificate.
@@ -434,20 +329,8 @@
      */
     private boolean isSchemaPackageAccessible(
             @NonNull String prefix, @NonNull String prefixedSchema, int callerUid) {
-        Map<String, Set<PackageIdentifier>> schemaToPackageIdentifierMap =
-                mPackageAccessibleMap.get(prefix);
-        if (schemaToPackageIdentifierMap == null) {
-            // No schemas under this prefix have granted package access, return early.
-            return false;
-        }
-
         Set<PackageIdentifier> packageIdentifiers =
-                schemaToPackageIdentifierMap.get(prefixedSchema);
-        if (packageIdentifiers == null) {
-            // No package identifiers were granted access for this schema, return early.
-            return false;
-        }
-
+                mPackageAccessibleMap.getAccessiblePackages(prefix, prefixedSchema);
         for (PackageIdentifier packageIdentifier : packageIdentifiers) {
             // Check that the caller uid matches this allowlisted PackageIdentifier.
             // TODO(b/169883602): Consider caching the UIDs of packages. Looking this up in the
@@ -467,7 +350,6 @@
                 return true;
             }
         }
-
         // If we can't verify the schema is package accessible, default to no access.
         return false;
     }
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/NotPlatformSurfaceableMap.java b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/NotPlatformSurfaceableMap.java
new file mode 100644
index 0000000..5afdda2
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/NotPlatformSurfaceableMap.java
@@ -0,0 +1,65 @@
+/*
+ * 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.appsearch.visibilitystore;
+
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Stores information about what types are hidden from platform surfaces through the
+ * {@link android.app.appsearch.SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem} API.
+ *
+ * This object is not thread safe.
+ * @hide
+ */
+public class NotPlatformSurfaceableMap {
+    /**
+     * Maps prefixes to the set of prefixed schemas that are platform-hidden within that prefix.
+     */
+    private final Map<String, Set<String>> mMap = new ArrayMap<>();
+
+    /**
+     * Sets the prefixed schemas that are opted out of platform surfacing for the prefix.
+     *
+     * <p>Any existing mappings for this prefix are overwritten.
+     */
+    public void setNotPlatformSurfaceable(@NonNull String prefix, @NonNull Set<String> schemas) {
+        mMap.put(prefix, schemas);
+    }
+
+    /**
+     * Returns whether the given prefixed schema is platform surfaceable (has not opted out) in the
+     * given prefix.
+     */
+    public boolean isSchemaPlatformSurfaceable(@NonNull String prefix, @NonNull String schemaType) {
+        Set<String> schemaTypes = mMap.get(prefix);
+        if (schemaTypes == null) {
+            // No opt-outs for this prefix
+            return true;
+        }
+        // Some schemas were opted out of being platform-surfaced. As long as this schema
+        // isn't one of those opt-outs, it's surfaceable.
+        return !schemaTypes.contains(schemaType);
+    }
+
+    /** Discards all data in the map. */
+    public void clear() {
+        mMap.clear();
+    }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/PackageAccessibleDocument.java b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/PackageAccessibleDocument.java
new file mode 100644
index 0000000..5601ef9
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/PackageAccessibleDocument.java
@@ -0,0 +1,107 @@
+/*
+ * 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.appsearch.visibilitystore;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.PackageIdentifier;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Holds configuration about a package+cert that can access a schema.
+ *
+ * @see android.app.appsearch.SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage
+ * @hide
+ */
+public class PackageAccessibleDocument extends GenericDocument {
+    /** Schema type for nested documents that hold package accessible information. */
+    public static final String SCHEMA_TYPE = "PackageAccessibleType";
+
+    /** Property that holds the package name that can access a schema. */
+    private static final String PACKAGE_NAME_PROPERTY = "packageName";
+
+    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+    private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
+
+    /** Property that holds the prefixed schema type that is accessible by some package. */
+    private static final String ACCESSIBLE_SCHEMA_PROPERTY = "accessibleSchema";
+
+    /**
+     * Schema for package accessible documents, these will be nested in a top-level
+     * {@link VisibilityDocument}.
+     *
+     * <p>NOTE: If you update this, also update
+     * {@link com.android.server.appsearch.external.localstorage.VisibilityStore#SCHEMA_VERSION}
+     */
+    public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    ACCESSIBLE_SCHEMA_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            .build();
+
+    public PackageAccessibleDocument(@NonNull GenericDocument genericDocument) {
+        super(genericDocument);
+    }
+
+    @Nullable
+    public String getAccessibleSchemaType() {
+        return getPropertyString(ACCESSIBLE_SCHEMA_PROPERTY);
+    }
+
+    /** Gets which package is able to access {@link #getAccessibleSchemaType} */
+    @NonNull
+    public PackageIdentifier getPackageIdentifier() {
+        String packageName = getPropertyString(PACKAGE_NAME_PROPERTY);
+        byte[] sha256Cert = getPropertyBytes(SHA_256_CERT_PROPERTY);
+        return new PackageIdentifier(packageName, sha256Cert);
+    }
+
+    /** Builder for {@link PackageAccessibleDocument} instances. */
+    public static class Builder extends GenericDocument.Builder<PackageAccessibleDocument.Builder> {
+        public Builder(@NonNull String namespace, @NonNull String id) {
+            super(namespace, id, SCHEMA_TYPE);
+        }
+
+        /** Sets which prefixed schema type is accessible by the package */
+        @NonNull
+        public Builder setAccessibleSchemaType(@NonNull String schemaType) {
+            return setPropertyString(ACCESSIBLE_SCHEMA_PROPERTY, schemaType);
+        }
+
+        /** Sets which package is able to access the {@link #setAccessibleSchemaType}. */
+        @NonNull
+        public Builder setPackageIdentifier(@NonNull PackageIdentifier packageIdentifier) {
+            return setPropertyString(PACKAGE_NAME_PROPERTY, packageIdentifier.getPackageName())
+                    .setPropertyBytes(SHA_256_CERT_PROPERTY,
+                            packageIdentifier.getSha256Certificate());
+        }
+
+        @Override
+        @NonNull
+        public PackageAccessibleDocument build() {
+            return new PackageAccessibleDocument(super.build());
+        }
+    }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/PackageAccessibleMap.java b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/PackageAccessibleMap.java
new file mode 100644
index 0000000..e90e8bf
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/PackageAccessibleMap.java
@@ -0,0 +1,74 @@
+/*
+ * 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.appsearch.visibilitystore;
+
+import android.annotation.NonNull;
+import android.app.appsearch.PackageIdentifier;
+import android.util.ArrayMap;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Stores information about what types are accessible to which packages through the
+ * {@link android.app.appsearch.SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage} API.
+ *
+ * This object is not thread safe.
+ * @hide
+ */
+public class PackageAccessibleMap {
+    /**
+     * Maps prefixes to prefixed schema types to PackageIdentifiers that have access to that schema.
+     */
+    private final Map<String, Map<String, Set<PackageIdentifier>>> mMap = new ArrayMap<>();
+
+    /**
+     * Sets the prefixed schemas that have package visibility in the given prefix.
+     *
+     * <p>Any existing mappings for this prefix are overwritten.
+     */
+    public void setPackageAccessible(
+            @NonNull String prefix,
+            @NonNull Map<String, Set<PackageIdentifier>> schemaToPackageIdentifier) {
+        mMap.put(prefix, schemaToPackageIdentifier);
+    }
+
+    /**
+     * Returns the set of all {@link android.app.appsearch.PackageIdentifier}s which can access the
+     * given schema type.
+     *
+     * <p>If no such settings exist, returns the empty set.
+     */
+    @NonNull
+    public Set<PackageIdentifier> getAccessiblePackages(
+            @NonNull String prefix, @NonNull String schemaType) {
+        Map<String, Set<PackageIdentifier>> schemaTypeToVisibility = mMap.get(prefix);
+        if (schemaTypeToVisibility == null) {
+            return Collections.emptySet();
+        }
+        Set<PackageIdentifier> accessiblePackages = schemaTypeToVisibility.get(schemaType);
+        if (accessiblePackages == null) {
+            return Collections.emptySet();
+        }
+        return accessiblePackages;
+    }
+
+    /** Discards all data in the map. */
+    public void clear() {
+        mMap.clear();
+    }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/VisibilityDocument.java b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/VisibilityDocument.java
new file mode 100644
index 0000000..327ce85
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/visibilitystore/VisibilityDocument.java
@@ -0,0 +1,92 @@
+/*
+ * 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.appsearch.visibilitystore;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Holds the visibility settings that apply to a package's databases.
+ * @hide
+ */
+public class VisibilityDocument extends GenericDocument {
+    /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
+    public static final String SCHEMA_TYPE = "VisibilityType";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    private static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable";
+
+    /** Property that holds nested documents of package accessible schemas. */
+    private static final String PACKAGE_ACCESSIBLE_PROPERTY = "packageAccessible";
+
+    /**
+     * Schema for the VisibilityStore's documents.
+     *
+     * <p>NOTE: If you update this, also update
+     * {@link com.android.server.appsearch.external.localstorage.VisibilityStore#SCHEMA_VERSION}
+     */
+    public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                    NOT_PLATFORM_SURFACEABLE_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                    PACKAGE_ACCESSIBLE_PROPERTY, PackageAccessibleDocument.SCHEMA_TYPE)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .build();
+
+    public VisibilityDocument(@NonNull GenericDocument genericDocument) {
+        super(genericDocument);
+    }
+
+    @Nullable
+    public String[] getNotPlatformSurfaceableSchemas() {
+        return getPropertyStringArray(NOT_PLATFORM_SURFACEABLE_PROPERTY);
+    }
+
+    @Nullable
+    public GenericDocument[] getPackageAccessibleSchemas() {
+        return getPropertyDocumentArray(PACKAGE_ACCESSIBLE_PROPERTY);
+    }
+
+    /** Builder for {@link VisibilityDocument}. */
+    public static class Builder extends GenericDocument.Builder<VisibilityDocument.Builder> {
+        public Builder(@NonNull String namespace, @NonNull String id) {
+            super(namespace, id, SCHEMA_TYPE);
+        }
+
+        /** Sets which prefixed schemas have opted out of platform surfacing. */
+        @NonNull
+        public Builder setSchemasNotPlatformSurfaceable(
+                @NonNull String[] notPlatformSurfaceableSchemas) {
+            return setPropertyString(
+                    NOT_PLATFORM_SURFACEABLE_PROPERTY, notPlatformSurfaceableSchemas);
+        }
+
+        /** Sets which prefixed schemas have configured package access. */
+        @NonNull
+        public Builder setPackageAccessibleSchemas(
+                @NonNull PackageAccessibleDocument[] packageAccessibleSchemas) {
+            return setPropertyDocument(PACKAGE_ACCESSIBLE_PROPERTY, packageAccessibleSchemas);
+        }
+    }
+}