| /* |
| * Copyright 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.external.localstorage.visibilitystore; |
| |
| import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.appsearch.AppSearchResult; |
| import android.app.appsearch.AppSearchSchema; |
| import android.app.appsearch.GenericDocument; |
| import android.app.appsearch.GetSchemaResponse; |
| import android.app.appsearch.VisibilityDocument; |
| import android.app.appsearch.VisibilityPermissionDocument; |
| import android.app.appsearch.exceptions.AppSearchException; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| |
| import com.android.server.appsearch.external.localstorage.AppSearchImpl; |
| import com.android.server.appsearch.external.localstorage.util.PrefixUtil; |
| |
| import com.google.android.icing.proto.PersistType; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * Stores all visibility settings for all databases that AppSearchImpl knows about. Persists the |
| * visibility settings and reloads them on initialization. |
| * |
| * <p>The VisibilityStore creates a {@link VisibilityDocument} for each schema. This document holds |
| * the visibility settings that apply to that schema. The VisibilityStore also creates a schema for |
| * these documents and has its own package and database so that its data doesn't interfere with any |
| * clients' data. It persists the document and schema through AppSearchImpl. |
| * |
| * <p>These visibility settings won't be used in AppSearch Jetpack, we only store them for clients |
| * to look up. |
| * |
| * @hide |
| */ |
| public class VisibilityStore { |
| private static final String TAG = "AppSearchVisibilityStor"; |
| /** |
| * These cannot have any of the special characters used by AppSearchImpl (e.g. {@code |
| * AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}. |
| */ |
| public static final String VISIBILITY_PACKAGE_NAME = "VS#Pkg"; |
| |
| static final String VISIBILITY_DATABASE_NAME = "VS#Db"; |
| |
| /** |
| * Map of PrefixedSchemaType and VisibilityDocument stores visibility information for each |
| * schema type. |
| */ |
| private final Map<String, VisibilityDocument> mVisibilityDocumentMap = new ArrayMap<>(); |
| |
| private final AppSearchImpl mAppSearchImpl; |
| |
| public VisibilityStore(@NonNull AppSearchImpl appSearchImpl) throws AppSearchException { |
| mAppSearchImpl = Objects.requireNonNull(appSearchImpl); |
| |
| GetSchemaResponse getSchemaResponse = |
| mAppSearchImpl.getSchema( |
| VISIBILITY_PACKAGE_NAME, |
| VISIBILITY_DATABASE_NAME, |
| new CallerAccess(/*callingPackageName=*/ VISIBILITY_PACKAGE_NAME)); |
| List<VisibilityDocumentV1> visibilityDocumentsV1s = null; |
| switch (getSchemaResponse.getVersion()) { |
| case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE: |
| // TODO (b/202194495) add VisibilityDocument in version 0 back instead of using |
| // GenericDocument. |
| List<GenericDocument> visibilityDocumentsV0s = |
| VisibilityStoreMigrationHelperFromV0.getVisibilityDocumentsInVersion0( |
| getSchemaResponse, mAppSearchImpl); |
| visibilityDocumentsV1s = |
| VisibilityStoreMigrationHelperFromV0.toVisibilityDocumentV1( |
| visibilityDocumentsV0s); |
| // fall through |
| case VisibilityDocument.SCHEMA_VERSION_DOC_PER_SCHEMA: |
| if (visibilityDocumentsV1s == null) { |
| // We need to read VisibilityDocument in Version 1 from AppSearch instead of |
| // taking from the above step. |
| visibilityDocumentsV1s = |
| VisibilityStoreMigrationHelperFromV1.getVisibilityDocumentsInVersion1( |
| mAppSearchImpl); |
| } |
| setLatestSchemaAndDocuments( |
| VisibilityStoreMigrationHelperFromV1.toVisibilityDocumentsV2( |
| visibilityDocumentsV1s)); |
| break; |
| case VisibilityDocument.SCHEMA_VERSION_LATEST: |
| Set<AppSearchSchema> existingVisibilitySchema = getSchemaResponse.getSchemas(); |
| if (existingVisibilitySchema.contains(VisibilityDocument.SCHEMA) |
| && existingVisibilitySchema.contains(VisibilityPermissionDocument.SCHEMA)) { |
| // The latest Visibility schema is in AppSearch, we must find our schema type. |
| // Extract all stored Visibility Document into mVisibilityDocumentMap. |
| loadVisibilityDocumentMap(); |
| } else { |
| // We must have a broken schema. Reset it to the latest version. |
| // Do NOT set forceOverride to be true here. If you hit problem here it means |
| // you made a incompatible change in Visibility Schema without update the |
| // version number. You should bump the version number and create a |
| // VisibilityStoreMigrationHelper which can analyse the different between the |
| // old version and the new version to migration user's visibility settings. |
| mAppSearchImpl.setSchema( |
| VISIBILITY_PACKAGE_NAME, |
| VISIBILITY_DATABASE_NAME, |
| Arrays.asList( |
| VisibilityDocument.SCHEMA, VisibilityPermissionDocument.SCHEMA), |
| /*visibilityDocuments=*/ Collections.emptyList(), |
| /*forceOverride=*/ false, |
| /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST, |
| /*setSchemaStatsBuilder=*/ null); |
| } |
| break; |
| default: |
| // We must did something wrong. |
| throw new AppSearchException( |
| AppSearchResult.RESULT_INTERNAL_ERROR, |
| "Found unsupported visibility version: " + getSchemaResponse.getVersion()); |
| } |
| } |
| |
| /** |
| * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous {@link |
| * VisibilityDocument}s with same prefixed schema type will be overwritten. |
| * |
| * @param prefixedVisibilityDocuments List of prefixed {@link VisibilityDocument} which contains |
| * schema type's visibility information. |
| * @throws AppSearchException on AppSearchImpl error. |
| */ |
| public void setVisibility(@NonNull List<VisibilityDocument> prefixedVisibilityDocuments) |
| throws AppSearchException { |
| Objects.requireNonNull(prefixedVisibilityDocuments); |
| // Save new setting. |
| for (int i = 0; i < prefixedVisibilityDocuments.size(); i++) { |
| // put VisibilityDocument to AppSearchImpl and mVisibilityDocumentMap. If there is a |
| // VisibilityDocument with same prefixed schema exists, it will be replaced by new |
| // VisibilityDocument in both AppSearch and memory look up map. |
| VisibilityDocument prefixedVisibilityDocument = prefixedVisibilityDocuments.get(i); |
| mAppSearchImpl.putDocument( |
| VISIBILITY_PACKAGE_NAME, |
| VISIBILITY_DATABASE_NAME, |
| prefixedVisibilityDocument, |
| /*sendChangeNotifications=*/ false, |
| /*logger=*/ null); |
| mVisibilityDocumentMap.put( |
| prefixedVisibilityDocument.getId(), prefixedVisibilityDocument); |
| } |
| // Now that the visibility document has been written. Persist the newly written data. |
| mAppSearchImpl.persistToDisk(PersistType.Code.LITE); |
| } |
| |
| /** |
| * Remove the visibility setting for the given prefixed schema type from both AppSearch and |
| * memory look up map. |
| */ |
| public void removeVisibility(@NonNull Set<String> prefixedSchemaTypes) |
| throws AppSearchException { |
| for (String prefixedSchemaType : prefixedSchemaTypes) { |
| if (mVisibilityDocumentMap.remove(prefixedSchemaType) == null) { |
| // The deleted schema is not all-default setting, we need to remove its |
| // VisibilityDocument from Icing. |
| try { |
| mAppSearchImpl.remove( |
| VISIBILITY_PACKAGE_NAME, |
| VISIBILITY_DATABASE_NAME, |
| VisibilityDocument.NAMESPACE, |
| prefixedSchemaType, |
| /*removeStatsBuilder=*/ null); |
| } catch (AppSearchException e) { |
| if (e.getResultCode() == RESULT_NOT_FOUND) { |
| // We are trying to remove this visibility setting, so it's weird but seems |
| // to be fine if we cannot find it. |
| Log.e( |
| TAG, |
| "Cannot find visibility document for " |
| + prefixedSchemaType |
| + " to remove."); |
| return; |
| } |
| throw e; |
| } |
| } |
| } |
| } |
| |
| /** Gets the {@link VisibilityDocument} for the given prefixed schema type. */ |
| @Nullable |
| public VisibilityDocument getVisibility(@NonNull String prefixedSchemaType) { |
| return mVisibilityDocumentMap.get(prefixedSchemaType); |
| } |
| |
| /** |
| * Loads all stored latest {@link VisibilityDocument} from Icing, and put them into {@link |
| * #mVisibilityDocumentMap}. |
| */ |
| private void loadVisibilityDocumentMap() throws AppSearchException { |
| // Populate visibility settings set |
| List<String> cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes(); |
| for (int i = 0; i < cachedSchemaTypes.size(); i++) { |
| String prefixedSchemaType = cachedSchemaTypes.get(i); |
| String packageName = PrefixUtil.getPackageName(prefixedSchemaType); |
| if (packageName.equals(VISIBILITY_PACKAGE_NAME)) { |
| continue; // Our own package. Skip. |
| } |
| |
| VisibilityDocument visibilityDocument; |
| try { |
| // Note: We use the other clients' prefixed schema type as ids |
| visibilityDocument = |
| new VisibilityDocument( |
| mAppSearchImpl.getDocument( |
| VISIBILITY_PACKAGE_NAME, |
| VISIBILITY_DATABASE_NAME, |
| VisibilityDocument.NAMESPACE, |
| /*id=*/ prefixedSchemaType, |
| /*typePropertyPaths=*/ Collections.emptyMap())); |
| } catch (AppSearchException e) { |
| if (e.getResultCode() == RESULT_NOT_FOUND) { |
| // The schema has all default setting and we won't have a VisibilityDocument for |
| // it. |
| continue; |
| } |
| // Otherwise, this is some other error we should pass up. |
| throw e; |
| } |
| mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument); |
| } |
| } |
| |
| /** Set the latest version of {@link VisibilityDocument} and its schema to AppSearch. */ |
| private void setLatestSchemaAndDocuments(@NonNull List<VisibilityDocument> migratedDocuments) |
| throws AppSearchException { |
| // The latest schema type doesn't exist yet. Add it. Set forceOverride true to |
| // delete old schema. |
| mAppSearchImpl.setSchema( |
| VISIBILITY_PACKAGE_NAME, |
| VISIBILITY_DATABASE_NAME, |
| Arrays.asList(VisibilityDocument.SCHEMA, VisibilityPermissionDocument.SCHEMA), |
| /*visibilityDocuments=*/ Collections.emptyList(), |
| /*forceOverride=*/ true, |
| /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST, |
| /*setSchemaStatsBuilder=*/ null); |
| for (int i = 0; i < migratedDocuments.size(); i++) { |
| VisibilityDocument migratedDocument = migratedDocuments.get(i); |
| mVisibilityDocumentMap.put(migratedDocument.getId(), migratedDocument); |
| mAppSearchImpl.putDocument( |
| VISIBILITY_PACKAGE_NAME, |
| VISIBILITY_DATABASE_NAME, |
| migratedDocument, |
| /*sendChangeNotifications=*/ false, |
| /*logger=*/ null); |
| } |
| } |
| } |