1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appsearch.localstorage.visibilitystore;
18 
19 import androidx.annotation.RestrictTo;
20 import androidx.annotation.VisibleForTesting;
21 import androidx.appsearch.app.AppSearchResult;
22 import androidx.appsearch.app.AppSearchSchema;
23 import androidx.appsearch.app.GenericDocument;
24 import androidx.appsearch.app.GetSchemaResponse;
25 import androidx.appsearch.app.PackageIdentifier;
26 import androidx.appsearch.exceptions.AppSearchException;
27 import androidx.appsearch.localstorage.AppSearchImpl;
28 import androidx.appsearch.localstorage.util.PrefixUtil;
29 import androidx.collection.ArrayMap;
30 import androidx.core.util.Preconditions;
31 
32 import org.jspecify.annotations.NonNull;
33 
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 
40 /**
41  * The helper class to store Visibility Document information of version 0 and handle the upgrade to
42  * version 1.
43  *
44  * @exportToFramework:hide
45  */
46 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
47 public class VisibilityStoreMigrationHelperFromV0 {
VisibilityStoreMigrationHelperFromV0()48     private VisibilityStoreMigrationHelperFromV0() {}
49     /** Prefix to add to all visibility document ids. IcingSearchEngine doesn't allow empty ids. */
50     private static final String DEPRECATED_ID_PREFIX = "uri:";
51 
52     /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
53     @VisibleForTesting
54     static final String DEPRECATED_VISIBILITY_SCHEMA_TYPE = "VisibilityType";
55 
56     /**
57      * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
58      */
59     @VisibleForTesting
60     static final String DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY =
61             "notPlatformSurfaceable";
62 
63     /** Property that holds nested documents of package accessible schemas. */
64     @VisibleForTesting
65     static final String DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY = "packageAccessible";
66 
67     /**
68      * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
69      */
70     @VisibleForTesting
71     static final String DEPRECATED_PACKAGE_SCHEMA_TYPE = "PackageAccessibleType";
72 
73     /** Property that holds the prefixed schema type that is accessible by some package. */
74     @VisibleForTesting
75     static final String DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY = "accessibleSchema";
76 
77     /** Property that holds the package name that can access a schema. */
78     @VisibleForTesting
79     static final String DEPRECATED_PACKAGE_NAME_PROPERTY = "packageName";
80 
81     /** Property that holds the SHA 256 certificate of the app that can access a schema. */
82     @VisibleForTesting
83     static final String DEPRECATED_SHA_256_CERT_PROPERTY = "sha256Cert";
84 
85 //    The visibility schema of version 0.
86 //---------------------------------------------------------------------------------------------
87 //    Schema of DEPRECATED_VISIBILITY_SCHEMA_TYPE:
88 //    new AppSearchSchema.Builder(
89 //            DEPRECATED_VISIBILITY_SCHEMA_TYPE)
90 //            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
91 //                    DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
92 //                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
93 //                    .build())
94 //            .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
95 //                    DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
96 //                    DEPRECATED_PACKAGE_SCHEMA_TYPE)
97 //                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
98 //                    .build())
99 //            .build();
100 //    Schema of DEPRECATED_PACKAGE_SCHEMA_TYPE:
101 //    new AppSearchSchema.Builder(DEPRECATED_PACKAGE_SCHEMA_TYPE)
102 //        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
103 //                 DEPRECATED_PACKAGE_NAME_PROPERTY)
104 //                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
105 //                .build())
106 //        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
107 //                DEPRECATED_SHA_256_CERT_PROPERTY)
108 //                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
109 //                .build())
110 //        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
111 //                DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY)
112 //                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
113 //                .build())
114 //        .build();
115 //---------------------------------------------------------------------------------------------
116 
117     /** Returns whether the given schema type is deprecated.     */
isDeprecatedType(@onNull String schemaType)118     static boolean isDeprecatedType(@NonNull String schemaType) {
119         return schemaType.equals(DEPRECATED_VISIBILITY_SCHEMA_TYPE)
120                 || schemaType.equals(DEPRECATED_PACKAGE_SCHEMA_TYPE);
121     }
122 
123     /**
124      * Adds a prefix to create a deprecated visibility document's id.
125      *
126      * @param packageName Package to which the visibility doc refers.
127      * @param databaseName Database to which the visibility doc refers.
128      * @return deprecated visibility document's id.
129      */
getDeprecatedVisibilityDocumentId( @onNull String packageName, @NonNull String databaseName)130     static @NonNull String getDeprecatedVisibilityDocumentId(
131             @NonNull String packageName, @NonNull String databaseName) {
132         return DEPRECATED_ID_PREFIX + PrefixUtil.createPrefix(packageName, databaseName);
133     }
134 
135     /**  Reads all stored deprecated Visibility Document in version 0 from icing. */
getVisibilityDocumentsInVersion0( @onNull GetSchemaResponse getSchemaResponse, @NonNull AppSearchImpl appSearchImpl)136     static List<GenericDocument> getVisibilityDocumentsInVersion0(
137             @NonNull GetSchemaResponse getSchemaResponse,
138             @NonNull AppSearchImpl appSearchImpl) throws AppSearchException {
139         if (!hasDeprecatedType(getSchemaResponse)) {
140             return new ArrayList<>();
141         }
142         Map<String, Set<String>> packageToDatabases = appSearchImpl.getPackageToDatabases();
143         List<GenericDocument> deprecatedDocuments = new ArrayList<>(packageToDatabases.size());
144         for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) {
145             String packageName = entry.getKey();
146             if (packageName.equals(VisibilityStore.VISIBILITY_PACKAGE_NAME)) {
147                 continue; // Our own package. Skip.
148             }
149             for (String databaseName : entry.getValue()) {
150                 try {
151                     // Note: We use the other clients' prefixed names as ids
152                     deprecatedDocuments.add(appSearchImpl.getDocument(
153                             VisibilityStore.VISIBILITY_PACKAGE_NAME,
154                             VisibilityStore.DOCUMENT_VISIBILITY_DATABASE_NAME,
155                             VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
156                             getDeprecatedVisibilityDocumentId(packageName, databaseName),
157                             /*typePropertyPaths=*/ Collections.emptyMap()));
158                 } catch (AppSearchException e) {
159                     if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
160                         // TODO(b/172068212): This indicates some desync error. We were expecting a
161                         //  document, but didn't find one. Should probably reset AppSearch instead
162                         //  of ignoring it.
163                         continue;
164                     }
165                     // Otherwise, this is some other error we should pass up.
166                     throw e;
167                 }
168             }
169         }
170         return deprecatedDocuments;
171     }
172 
173     /**
174      * Converts the given list of deprecated Visibility Documents into a Map of {@code
175      * <PrefixedSchemaType, VisibilityDocument.Builder of the latest version>}.
176      *
177      * @param visibilityDocumentV0s          The deprecated Visibility Document we found.
178      */
toVisibilityDocumentV1( @onNull List<GenericDocument> visibilityDocumentV0s)179     static @NonNull List<VisibilityDocumentV1> toVisibilityDocumentV1(
180             @NonNull List<GenericDocument> visibilityDocumentV0s) {
181         Map<String, VisibilityDocumentV1.Builder> documentBuilderMap = new ArrayMap<>();
182 
183         // Set all visibility information into documentBuilderMap
184         for (int i = 0; i < visibilityDocumentV0s.size(); i++) {
185             GenericDocument visibilityDocumentV0 = visibilityDocumentV0s.get(i);
186 
187             // Read not displayed by system property field.
188             String[] notDisplayedBySystemSchemas = visibilityDocumentV0.getPropertyStringArray(
189                     DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
190             if (notDisplayedBySystemSchemas != null) {
191                 for (String notDisplayedBySystemSchema : notDisplayedBySystemSchemas) {
192                     // SetSchemaRequest.Builder.build() make sure all schemas that has visibility
193                     // setting must present in the requests.
194                     VisibilityDocumentV1.Builder visibilityBuilder = getOrCreateBuilder(
195                             documentBuilderMap, notDisplayedBySystemSchema);
196                     visibilityBuilder.setNotDisplayedBySystem(true);
197                 }
198             }
199 
200             // Read visible to packages field.
201             GenericDocument[] deprecatedPackageDocuments = visibilityDocumentV0
202                     .getPropertyDocumentArray(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY);
203             if (deprecatedPackageDocuments != null) {
204                 for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
205                     String prefixedSchemaType = Preconditions.checkNotNull(
206                             deprecatedPackageDocument.getPropertyString(
207                                     DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
208                     VisibilityDocumentV1.Builder visibilityBuilder =
209                             getOrCreateBuilder(documentBuilderMap, prefixedSchemaType);
210                     String packageName = Preconditions.checkNotNull(
211                             deprecatedPackageDocument.getPropertyString(
212                                     DEPRECATED_PACKAGE_NAME_PROPERTY));
213                     byte[] sha256Cert = Preconditions.checkNotNull(
214                             deprecatedPackageDocument.getPropertyBytes(
215                                     DEPRECATED_SHA_256_CERT_PROPERTY));
216                     visibilityBuilder.addVisibleToPackage(
217                             new PackageIdentifier(packageName, sha256Cert));
218                 }
219             }
220         }
221         List<VisibilityDocumentV1> visibilityDocumentsV1 =
222                 new ArrayList<>(documentBuilderMap.size());
223         for (Map.Entry<String, VisibilityDocumentV1.Builder> entry :
224                 documentBuilderMap.entrySet()) {
225             visibilityDocumentsV1.add(entry.getValue().build());
226         }
227         return visibilityDocumentsV1;
228     }
229 
230     /**
231      * Return whether the database maybe has the oldest version of deprecated schema.
232      *
233      * <p> Since the current version number is 0, it is possible that the database is just empty
234      * and it return 0 as the default version number. So we need to check if the deprecated document
235      * presents to trigger the migration.
236      */
hasDeprecatedType(@onNull GetSchemaResponse getSchemaResponse)237     private static boolean hasDeprecatedType(@NonNull GetSchemaResponse getSchemaResponse) {
238         for (AppSearchSchema schema : getSchemaResponse.getSchemas()) {
239             if (VisibilityStoreMigrationHelperFromV0
240                     .isDeprecatedType(schema.getSchemaType())) {
241                 // Found deprecated type, we need to migrate visibility Document. And it's
242                 // not possible for us to find the latest visibility schema.
243                 return true;
244             }
245         }
246         return false;
247     }
248 
getOrCreateBuilder( @onNull Map<String, VisibilityDocumentV1.Builder> documentBuilderMap, @NonNull String schemaType)249     private static VisibilityDocumentV1.@NonNull Builder getOrCreateBuilder(
250             @NonNull Map<String, VisibilityDocumentV1.Builder> documentBuilderMap,
251             @NonNull String schemaType) {
252         VisibilityDocumentV1.Builder builder = documentBuilderMap.get(schemaType);
253         if (builder == null) {
254             builder = new VisibilityDocumentV1.Builder(/*id=*/ schemaType);
255             documentBuilderMap.put(schemaType, builder);
256         }
257         return builder;
258     }
259 }
260