blob: b8cd0ade2a6a67f407857e295af5804ddbfdbda2 [file] [log] [blame]
/*
* 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);
}
}
}