VisibilityStoreMigrationHelperFromV0.java

/*
 * Copyright 2022 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 androidx.appsearch.localstorage.visibilitystore;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.AppSearchSchema;
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.GetSchemaResponse;
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.VisibilityDocument;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.localstorage.AppSearchImpl;
import androidx.appsearch.localstorage.util.PrefixUtil;
import androidx.collection.ArrayMap;
import androidx.core.util.Preconditions;

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
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
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 GetSchemaResponse getSchemaResponse,
            @NonNull AppSearchImpl appSearchImpl) throws AppSearchException {
        if (!hasDeprecatedType(getSchemaResponse)) {
            return new ArrayList<>();
        }
        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.VISIBILITY_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.VISIBILITY_PACKAGE_NAME,
                            VisibilityStore.VISIBILITY_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 visibilityDocumentV0s          The deprecated Visibility Document we found.
     */
    @NonNull
    static List<VisibilityDocumentV1> toVisibilityDocumentV1(
            @NonNull List<GenericDocument> visibilityDocumentV0s) {
        Map<String, VisibilityDocumentV1.Builder> documentBuilderMap = new ArrayMap<>();

        // Set all visibility information into documentBuilderMap
        for (int i = 0; i < visibilityDocumentV0s.size(); i++) {
            GenericDocument visibilityDocumentV0 = visibilityDocumentV0s.get(i);

            // Read not displayed by system property field.
            String[] notDisplayedBySystemSchemas = visibilityDocumentV0.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.
                    VisibilityDocumentV1.Builder visibilityBuilder = getOrCreateBuilder(
                            documentBuilderMap, notDisplayedBySystemSchema);
                    visibilityBuilder.setNotDisplayedBySystem(true);
                }
            }

            // Read visible to packages field.
            GenericDocument[] deprecatedPackageDocuments = visibilityDocumentV0
                    .getPropertyDocumentArray(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY);
            if (deprecatedPackageDocuments != null) {
                for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
                    String prefixedSchemaType = Preconditions.checkNotNull(
                            deprecatedPackageDocument.getPropertyString(
                            DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
                    VisibilityDocumentV1.Builder visibilityBuilder =
                            getOrCreateBuilder(documentBuilderMap, prefixedSchemaType);
                    String packageName = Preconditions.checkNotNull(
                            deprecatedPackageDocument.getPropertyString(
                                DEPRECATED_PACKAGE_NAME_PROPERTY));
                    byte[] sha256Cert = Preconditions.checkNotNull(
                            deprecatedPackageDocument.getPropertyBytes(
                                DEPRECATED_SHA_256_CERT_PROPERTY));
                    visibilityBuilder.addVisibleToPackage(
                            new PackageIdentifier(packageName, sha256Cert));
                }
            }
        }
        List<VisibilityDocumentV1> visibilityDocumentsV1 =
                new ArrayList<>(documentBuilderMap.size());
        for (Map.Entry<String, VisibilityDocumentV1.Builder> entry :
                documentBuilderMap.entrySet()) {
            visibilityDocumentsV1.add(entry.getValue().build());
        }
        return visibilityDocumentsV1;
    }

    /**
     * Return whether the database maybe has the oldest version of deprecated schema.
     *
     * <p> Since 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 static boolean hasDeprecatedType(@NonNull GetSchemaResponse getSchemaResponse) {
        for (AppSearchSchema schema : getSchemaResponse.getSchemas()) {
            if (VisibilityStoreMigrationHelperFromV0
                    .isDeprecatedType(schema.getSchemaType())) {
                // Found deprecated type, we need to migrate visibility Document. And it's
                // not possible for us to find the latest visibility schema.
                return true;
            }
        }
        return false;
    }

    @NonNull
    private static VisibilityDocumentV1.Builder getOrCreateBuilder(
            @NonNull Map<String, VisibilityDocumentV1.Builder> documentBuilderMap,
            @NonNull String schemaType) {
        VisibilityDocumentV1.Builder builder = documentBuilderMap.get(schemaType);
        if (builder == null) {
            builder = new VisibilityDocumentV1.Builder(/*id=*/ schemaType);
            documentBuilderMap.put(schemaType, builder);
        }
        return builder;
    }
}