Add visibility migration helper and test.

We upgrade Visibility schema from version 0 to 1.
Version 0: one VisibilityDocument contains all visibility information
for whole package.
Version 1: One VisibilityDocument contains visibility information for
a single schema.

We need a migration plan to ensure upgrading devices won't lost
any information.

Test: VisibilityStoreMigrationHelperTest
Bug: 203736617
Change-Id: I405fe8c130d3c88a69ec4360a83b07faa33b6d0d
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
index 114cf8b..e822faa 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
@@ -36,7 +36,7 @@
      */
     String PACKAGE_NAME = "VS#Pkg";
 
-    @VisibleForTesting String DATABASE_NAME = "VS#Db";
+    String DATABASE_NAME = "VS#Db";
 
     /**
      * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous
diff --git a/service/java/com/android/server/appsearch/visibilitystore/VisibilityStoreImpl.java b/service/java/com/android/server/appsearch/visibilitystore/VisibilityStoreImpl.java
index 01379db..c986798 100644
--- a/service/java/com/android/server/appsearch/visibilitystore/VisibilityStoreImpl.java
+++ b/service/java/com/android/server/appsearch/visibilitystore/VisibilityStoreImpl.java
@@ -16,10 +16,12 @@
 package com.android.server.appsearch.visibilitystore;
 
 import static android.Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA;
+import static android.app.appsearch.AppSearchResult.RESULT_INTERNAL_ERROR;
 import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND;
 
 import android.annotation.NonNull;
 import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
 import android.app.appsearch.GetSchemaResponse;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.content.Context;
@@ -60,6 +62,15 @@
  */
 public class VisibilityStoreImpl implements VisibilityStore {
     private static final String TAG = "AppSearchVisibilityStor";
+    // The initial schema version, one VisibilityDocument contains all visibility information for
+    // whole package.
+    private static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
+
+    // One VisibilityDocument contains visibility information for a single schema.
+    private static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
+
+    private static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_DOC_PER_SCHEMA;
+
     /** Version for the visibility schema */
     private static final int SCHEMA_VERSION = 1;
 
@@ -92,30 +103,21 @@
         mAppSearchImpl = Objects.requireNonNull(appSearchImpl);
         mUserContext = Objects.requireNonNull(userContext);
 
-        // TODO(b/202194495) handle schema migration from version 0 to 1.
         GetSchemaResponse getSchemaResponse =
             mAppSearchImpl.getSchema(PACKAGE_NAME, PACKAGE_NAME, DATABASE_NAME);
-        boolean hasVisibilityType = false;
-        for (AppSearchSchema schema : getSchemaResponse.getSchemas()) {
-            if (schema.getSchemaType().equals(VisibilityDocument.SCHEMA_TYPE)) {
-                hasVisibilityType = true;
-                // Found our type, can exit early.
+        switch (getSchemaResponse.getVersion()) {
+            case SCHEMA_VERSION_DOC_PER_PACKAGE:
+                maybeMigrateToLatest(getSchemaResponse);
                 break;
-            }
-        }
-        if (!hasVisibilityType) {
-            // The latest schema type doesn't exist yet. Add it.
-            mAppSearchImpl.setSchema(
-                    PACKAGE_NAME,
-                    DATABASE_NAME,
-                    Collections.singletonList(VisibilityDocument.SCHEMA),
-                    /*visibilityStore=*/ null,  // Avoid recursive calls
-                    /*prefixedVisibilityDocuments=*/ Collections.emptyList(),
-                    /*forceOverride=*/ false,
-                    /*version=*/ SCHEMA_VERSION,
-                    /*setSchemaStatsBuilder=*/ null);
-        } else {
-            loadVisibilityDocumentMap();
+            case SCHEMA_VERSION_LATEST:
+                // The latest Visibility schema is in AppSearch, we must find our schema type.
+                // Extract all stored Visibility Document into mVisibilityDocumentMap.
+                loadVisibilityDocumentMap();
+                break;
+            default:
+                // We must did something wrong.
+                throw new AppSearchException(RESULT_INTERNAL_ERROR,
+                        "Found unsupported visibility version: " + getSchemaResponse.getVersion());
         }
     }
 
@@ -134,7 +136,6 @@
             mVisibilityDocumentMap.put(prefixedVisibilityDocument.getId(),
                     prefixedVisibilityDocument);
         }
-
         // Now that the visibility document has been written. Persist the newly written data.
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
     }
@@ -288,4 +289,58 @@
             mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument);
         }
     }
+
+    /**
+     * Migrate the {@link VisibilityDocument} to the latest version.
+     *
+     * <p>If the current version number is 0, it is possible that the database is just empty and it
+     * return 0 as the default version number. So we need to check if the deprecated document
+     * presents to trigger the migration.
+     */
+    private void maybeMigrateToLatest(@NonNull GetSchemaResponse getSchemaResponse)
+            throws AppSearchException {
+        // The database maybe empty or has the oldest version of deprecated schema.
+        boolean hasDeprecatedType = false;
+        List<GenericDocument> deprecatedDocuments = null;
+        for (AppSearchSchema schema : getSchemaResponse.getSchemas()) {
+            if (VisibilityStoreMigrationHelperFromV0
+                    .isDeprecatedType(schema.getSchemaType())) {
+                hasDeprecatedType = true;
+                // Found deprecated type, we need to migrate visibility Document. And it's
+                // not possible for us to find the latest visibility schema.
+                break;
+            }
+        }
+        if (hasDeprecatedType) {
+            // Read deprecated Visibility Document, migrate them to the latest version and
+            // save in mVisibilityDocumentMap.
+            deprecatedDocuments = VisibilityStoreMigrationHelperFromV0
+                    .getVisibilityDocumentsInVersion0(mAppSearchImpl);
+        }
+        // The latest schema type doesn't exist yet. Add it. Set forceOverride true to
+        // delete old schema.
+        mAppSearchImpl.setSchema(
+                PACKAGE_NAME,
+                DATABASE_NAME,
+                Collections.singletonList(VisibilityDocument.SCHEMA),
+                /*visibilityStore=*/ null,  // Avoid recursive calls
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ true,
+                /*version=*/ SCHEMA_VERSION,
+                /*setSchemaStatsBuilder=*/ null);
+        if (hasDeprecatedType) {
+            Map<String, VisibilityDocument.Builder> documentBuilderMap =
+                    VisibilityStoreMigrationHelperFromV0.toVisibilityDocumentsV1(
+                            deprecatedDocuments);
+
+            // Build visibility document from builder and put to AppSearch
+            for (Map.Entry<String, VisibilityDocument.Builder> entry :
+                    documentBuilderMap.entrySet()) {
+                VisibilityDocument visibilityDocument = entry.getValue().build();
+                mVisibilityDocumentMap.put(entry.getKey(), visibilityDocument);
+                mAppSearchImpl.putDocument(PACKAGE_NAME, DATABASE_NAME,
+                        visibilityDocument, /*logger=*/ null);
+            }
+        }
+    }
 }
diff --git a/service/java/com/android/server/appsearch/visibilitystore/VisibilityStoreMigrationHelperFromV0.java b/service/java/com/android/server/appsearch/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
new file mode 100644
index 0000000..f3497bc
--- /dev/null
+++ b/service/java/com/android/server/appsearch/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
@@ -0,0 +1,225 @@
+/*
+ * 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.AppSearchResult;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.PackageIdentifier;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appsearch.external.localstorage.AppSearchImpl;
+import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
+import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The helper class to store Visibility Document information of version 0 and handle the upgrade to
+ * version 1.
+ *
+ * @hide
+ */
+public class VisibilityStoreMigrationHelperFromV0 {
+    private VisibilityStoreMigrationHelperFromV0() {}
+    /** Prefix to add to all visibility document ids. IcingSearchEngine doesn't allow empty ids. */
+    private static final String DEPRECATED_ID_PREFIX = "uri:";
+
+    /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
+    @VisibleForTesting
+    static final String DEPRECATED_VISIBILITY_SCHEMA_TYPE = "VisibilityType";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    @VisibleForTesting
+    static final String DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY =
+            "notPlatformSurfaceable";
+
+    /** Property that holds nested documents of package accessible schemas. */
+    @VisibleForTesting
+    static final String DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY = "packageAccessible";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    @VisibleForTesting
+    static final String DEPRECATED_PACKAGE_SCHEMA_TYPE = "PackageAccessibleType";
+
+    /** Property that holds the prefixed schema type that is accessible by some package. */
+    @VisibleForTesting
+    static final String DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY = "accessibleSchema";
+
+    /** Property that holds the package name that can access a schema. */
+    @VisibleForTesting
+    static final String DEPRECATED_PACKAGE_NAME_PROPERTY = "packageName";
+
+    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+    @VisibleForTesting
+    static final String DEPRECATED_SHA_256_CERT_PROPERTY = "sha256Cert";
+
+//    The visibility schema of version 0.
+//--------------------------------------------------------------------------------------------------
+//    Schema of DEPRECATED_VISIBILITY_SCHEMA_TYPE:
+//    new AppSearchSchema.Builder(
+//            DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+//            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+//                    DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+//                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+//                    .build())
+//            .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+//                    DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+//                    DEPRECATED_PACKAGE_SCHEMA_TYPE)
+//                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+//                    .build())
+//            .build();
+//    Schema of DEPRECATED_PACKAGE_SCHEMA_TYPE:
+//    new AppSearchSchema.Builder(DEPRECATED_PACKAGE_SCHEMA_TYPE)
+//        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+//                 DEPRECATED_PACKAGE_NAME_PROPERTY)
+//                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+//                .build())
+//        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+//                DEPRECATED_SHA_256_CERT_PROPERTY)
+//                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+//                .build())
+//        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+//                DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY)
+//                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+//                .build())
+//        .build();
+//--------------------------------------------------------------------------------------------------
+
+    /** Returns whether the given schema type is deprecated.     */
+    static boolean isDeprecatedType(@NonNull String schemaType) {
+        return schemaType.equals(DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+                || schemaType.equals(DEPRECATED_PACKAGE_SCHEMA_TYPE);
+    }
+
+    /**
+     * Adds a prefix to create a deprecated visibility document's id.
+     *
+     * @param packageName Package to which the visibility doc refers.
+     * @param databaseName Database to which the visibility doc refers.
+     * @return deprecated visibility document's id.
+     */
+    @NonNull
+    static String getDeprecatedVisibilityDocumentId(
+            @NonNull String packageName, @NonNull String databaseName) {
+        return DEPRECATED_ID_PREFIX + PrefixUtil.createPrefix(packageName, databaseName);
+    }
+
+    /**  Reads all stored deprecated Visibility Document in version 0 from icing. */
+    static List<GenericDocument> getVisibilityDocumentsInVersion0(
+            @NonNull AppSearchImpl appSearchImpl) throws AppSearchException {
+        Map<String, Set<String>> packageToDatabases = appSearchImpl.getPackageToDatabases();
+        List<GenericDocument> deprecatedDocuments = new ArrayList<>(packageToDatabases.size());
+        for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) {
+            String packageName = entry.getKey();
+            if (packageName.equals(VisibilityStore.PACKAGE_NAME)) {
+                continue; // Our own package. Skip.
+            }
+            for (String databaseName : entry.getValue()) {
+                try {
+                    // Note: We use the other clients' prefixed names as ids
+                    deprecatedDocuments.add(appSearchImpl.getDocument(
+                            VisibilityStore.PACKAGE_NAME,
+                            VisibilityStore.DATABASE_NAME,
+                            VisibilityDocument.NAMESPACE,
+                            getDeprecatedVisibilityDocumentId(packageName, databaseName),
+                            /*typePropertyPaths=*/ Collections.emptyMap()));
+                } catch (AppSearchException e) {
+                    if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
+                        // TODO(b/172068212): This indicates some desync error. We were expecting a
+                        //  document, but didn't find one. Should probably reset AppSearch instead
+                        //  of ignoring it.
+                        continue;
+                    }
+                    // Otherwise, this is some other error we should pass up.
+                    throw e;
+                }
+            }
+        }
+        return deprecatedDocuments;
+    }
+
+    /**
+     * Converts the given list of deprecated Visibility Documents into a Map of {@code
+     * <PrefixedSchemaType, VisibilityDocument.Builder of the latest version>}.
+     *
+     * @param deprecatedDocuments          The deprecated Visibility Document we found.
+     */
+    @NonNull
+    static Map<String, VisibilityDocument.Builder> toVisibilityDocumentsV1(
+            @NonNull List<GenericDocument> deprecatedDocuments) {
+        Map<String, VisibilityDocument.Builder> documentBuilderMap = new ArrayMap<>();
+
+        // Set all visibility information into documentBuilderMap
+        for (int i = 0; i < deprecatedDocuments.size(); i++) {
+            GenericDocument deprecatedDocument = deprecatedDocuments.get(i);
+
+            // Read not displayed by system property field.
+            String[] notDisplayedBySystemSchemas = deprecatedDocument.getPropertyStringArray(
+                    DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+            if (notDisplayedBySystemSchemas != null) {
+                for (String notDisplayedBySystemSchema : notDisplayedBySystemSchemas) {
+                    // SetSchemaRequest.Builder.build() make sure all schemas that has visibility
+                    // setting must present in the requests.
+                    VisibilityDocument.Builder visibilityBuilder = getOrCreateBuilder(
+                            documentBuilderMap, notDisplayedBySystemSchema);
+                    visibilityBuilder.setNotDisplayedBySystem(true);
+                }
+            }
+
+            // Read visible to packages field.
+            GenericDocument[] deprecatedPackageDocuments = deprecatedDocument
+                    .getPropertyDocumentArray(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY);
+            if (deprecatedPackageDocuments != null) {
+                for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
+                    String prefixedSchemaType = deprecatedPackageDocument
+                            .getPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY);
+                    VisibilityDocument.Builder visibilityBuilder = getOrCreateBuilder(
+                            documentBuilderMap, prefixedSchemaType);
+                    visibilityBuilder.addVisibleToPackage(new PackageIdentifier(
+                            deprecatedPackageDocument.getPropertyString(
+                                    DEPRECATED_PACKAGE_NAME_PROPERTY),
+                            deprecatedPackageDocument.getPropertyBytes(
+                                    DEPRECATED_SHA_256_CERT_PROPERTY)));
+                }
+            }
+        }
+        return documentBuilderMap;
+    }
+
+    @NonNull
+    private static VisibilityDocument.Builder getOrCreateBuilder(
+            @NonNull Map<String, VisibilityDocument.Builder> documentBuilderMap,
+            @NonNull String schemaType) {
+        VisibilityDocument.Builder builder = documentBuilderMap.get(schemaType);
+        if (builder == null) {
+            builder = new VisibilityDocument.Builder(/*id=*/ schemaType);
+            documentBuilderMap.put(schemaType, builder);
+        }
+        return builder;
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java b/testing/servicestests/src/com/android/server/appsearch/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
new file mode 100644
index 0000000..e50a349
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
@@ -0,0 +1,255 @@
+/*
+ * 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 static android.Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+
+import static com.android.server.appsearch.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY;
+import static com.android.server.appsearch.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY;
+import static com.android.server.appsearch.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_PACKAGE_NAME_PROPERTY;
+import static com.android.server.appsearch.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_PACKAGE_SCHEMA_TYPE;
+import static com.android.server.appsearch.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_SHA_256_CERT_PROPERTY;
+import static com.android.server.appsearch.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_VISIBILITY_SCHEMA_TYPE;
+import static com.android.server.appsearch.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.PackageIdentifier;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.appsearch.external.localstorage.AppSearchImpl;
+import com.android.server.appsearch.external.localstorage.OptimizeStrategy;
+import com.android.server.appsearch.external.localstorage.UnlimitedLimitConfig;
+import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
+import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Map;
+
+public class VisibilityStoreMigrationHelperFromV0Test {
+
+    /**
+     * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
+     */
+    private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+
+    @Rule
+    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+    private final Map<UserHandle, PackageManager> mMockPackageManagers = new ArrayMap<>();
+    private Context mContext;
+    private File mFile;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+        mContext = new ContextWrapper(context) {
+            @Override
+            public Context createContextAsUser(UserHandle user, int flags) {
+                return new ContextWrapper(super.createContextAsUser(user, flags)) {
+                    @Override
+                    public PackageManager getPackageManager() {
+                        return getMockPackageManager(user);
+                    }
+                };
+            }
+
+            @Override
+            public PackageManager getPackageManager() {
+                return createContextAsUser(getUser(), /*flags=*/ 0).getPackageManager();
+            }
+        };
+
+        // Give ourselves global query permissions
+        mFile = mTemporaryFolder.newFolder();
+    }
+
+    @Test
+    public void testVisibilityMigration_from0() throws Exception {
+        // Values for a "foo" client
+        String packageNameFoo = "packageFoo";
+        byte[] sha256CertFoo = new byte[32];
+
+        // Values for a "bar" client
+        String packageNameBar = "packageBar";
+        byte[] sha256CertBar = new byte[32];
+
+        // Create AppSearchImpl with visibility document version 0;
+        AppSearchImpl appSearchImplInV0 = buildAppSearchImplInV0();
+        // Build deprecated visibility documents in version 0
+        // "schema1" and "schema2" are platform hidden.
+        // "schema1" is accessible to packageFoo and "schema2" is accessible to packageBar.
+        String prefix = PrefixUtil.createPrefix("package", "database");
+        GenericDocument deprecatedVisibilityToPackageFoo = new GenericDocument.Builder<>(
+                VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema1")
+                .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameFoo)
+                .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertFoo)
+                .build();
+        GenericDocument deprecatedVisibilityToPackageBar = new GenericDocument.Builder<>(
+                VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema2")
+                .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameBar)
+                .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertBar)
+                .build();
+        GenericDocument deprecatedVisibilityDocument = new GenericDocument.Builder<>(
+                VisibilityDocument.NAMESPACE,
+                VisibilityStoreMigrationHelperFromV0.getDeprecatedVisibilityDocumentId(
+                        "package", "database"),
+                DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+                .setPropertyString(DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+                        prefix + "Schema1", prefix + "Schema2")
+                .setPropertyDocument(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+                        deprecatedVisibilityToPackageFoo, deprecatedVisibilityToPackageBar)
+                .build();
+
+        // Put deprecated visibility documents in version 0 to AppSearchImpl
+        appSearchImplInV0.putDocument(
+                VisibilityStore.PACKAGE_NAME,
+                VisibilityStore.DATABASE_NAME,
+                deprecatedVisibilityDocument,
+                /*logger=*/null);
+
+        // Set some client schemas into AppSearchImpl.
+        appSearchImplInV0.setSchema(
+                "package",
+                "database",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("schema1").build(),
+                        new AppSearchSchema.Builder("schema2").build()),
+                /*VisibilityStore=*/null,
+                /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*schemaVersion=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Persist to disk and re-open the AppSearchImpl
+        appSearchImplInV0.close();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Create Visibility store by AppSearchImpl, all deprecated visibility documents should be
+        // already migrated to version 1. And we won't lost any information.
+        VisibilityStoreImpl.create(appSearchImpl, mContext);
+
+        VisibilityDocument actualDocument1 = new VisibilityDocument(
+                appSearchImpl.getDocument(
+                        VisibilityStore.PACKAGE_NAME,
+                        VisibilityStore.DATABASE_NAME,
+                        VisibilityDocument.NAMESPACE,
+                        /*id=*/ prefix + "Schema1",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        VisibilityDocument actualDocument2 = new VisibilityDocument(
+                appSearchImpl.getDocument(
+                        VisibilityStore.PACKAGE_NAME,
+                        VisibilityStore.DATABASE_NAME,
+                        VisibilityDocument.NAMESPACE,
+                        /*id=*/ prefix + "Schema2",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+
+        VisibilityDocument expectedDocument1 =
+                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema1")
+                        .setNotDisplayedBySystem(true)
+                        .setCreationTimestampMillis(actualDocument1.getCreationTimestampMillis())
+                        .addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
+                        .build();
+        VisibilityDocument expectedDocument2 =
+                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema2")
+                        .setNotDisplayedBySystem(true)
+                        .setCreationTimestampMillis(actualDocument2.getCreationTimestampMillis())
+                        .addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
+                        .build();
+        assertThat(actualDocument1).isEqualTo(expectedDocument1);
+        assertThat(actualDocument2).isEqualTo(expectedDocument2);
+    }
+
+    /** Build AppSearchImpl with deprecated visibility schemas version 0.     */
+    private AppSearchImpl buildAppSearchImplInV0() throws Exception {
+        AppSearchSchema visibilityDocumentSchemaV0 = new AppSearchSchema.Builder(
+                DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                        DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+                        DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .build();
+        AppSearchSchema visibilityToPackagesSchemaV0 = new AppSearchSchema.Builder(
+                DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                        DEPRECATED_PACKAGE_NAME_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                        DEPRECATED_SHA_256_CERT_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                        DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .build();
+        // Set deprecated visibility schema version 0 into AppSearchImpl.
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+        appSearchImpl.setSchema(
+                VisibilityStore.PACKAGE_NAME,
+                VisibilityStore.DATABASE_NAME,
+                ImmutableList.of(visibilityDocumentSchemaV0, visibilityToPackagesSchemaV0),
+                /*VisibilityStore=*/null,
+                /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+        return appSearchImpl;
+    }
+
+    @NonNull
+    private PackageManager getMockPackageManager(@NonNull UserHandle user) {
+        PackageManager pm = mMockPackageManagers.get(user);
+        if (pm == null) {
+            pm = Mockito.mock(PackageManager.class);
+            mMockPackageManagers.put(user, pm);
+        }
+        return pm;
+    }
+}