blob: bf58d23ed29649a1408185e35153c2965e3a0b34 [file] [log] [blame]
/*
* Copyright 2020 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 android.app.appsearch;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.app.appsearch.annotation.CanIgnoreReturnValue;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Encapsulates a request to update the schema of an {@link AppSearchSession} database.
*
* <p>The schema is composed of a collection of {@link AppSearchSchema} objects, each of which
* defines a unique type of data.
*
* <p>The first call to SetSchemaRequest will set the provided schema and store it within the {@link
* AppSearchSession} database.
*
* <p>Subsequent calls will compare the provided schema to the previously saved schema, to determine
* how to treat existing documents.
*
* <p>The following types of schema modifications are always safe and are made without deleting any
* existing documents:
*
* <ul>
* <li>Addition of new {@link AppSearchSchema} types
* <li>Addition of new properties to an existing {@link AppSearchSchema} type
* <li>Changing the cardinality of a property to be less restrictive
* </ul>
*
* <p>The following types of schema changes are not backwards compatible:
*
* <ul>
* <li>Removal of an existing {@link AppSearchSchema} type
* <li>Removal of a property from an existing {@link AppSearchSchema} type
* <li>Changing the data type of an existing property
* <li>Changing the cardinality of a property to be more restrictive
* </ul>
*
* <p>Providing a schema with incompatible changes, will throw an {@link
* android.app.appsearch.exceptions.AppSearchException}, with a message describing the
* incompatibility. As a result, the previously set schema will remain unchanged.
*
* <p>Backward incompatible changes can be made by :
*
* <ul>
* <li>setting {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}. This
* deletes all documents that are incompatible with the new schema. The new schema is then
* saved and persisted to disk.
* <li>Add a {@link Migrator} for each incompatible type and make no deletion. The migrator will
* migrate documents from its old schema version to the new version. Migrated types will be
* set into both {@link SetSchemaResponse#getIncompatibleTypes()} and {@link
* SetSchemaResponse#getMigratedTypes()}. See the migration section below.
* </ul>
*
* @see AppSearchSession#setSchema
* @see Migrator
*/
public final class SetSchemaRequest {
/**
* List of Android Permission are supported in {@link
* SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
*
* @see android.Manifest.permission
* @hide
*/
@IntDef(
value = {
READ_SMS,
READ_CALENDAR,
READ_CONTACTS,
READ_EXTERNAL_STORAGE,
READ_HOME_APP_SEARCH_DATA,
READ_ASSISTANT_APP_SEARCH_DATA,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AppSearchSupportedPermission {}
/**
* The {@link android.Manifest.permission#READ_SMS} AppSearch supported in {@link
* SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
*/
public static final int READ_SMS = 1;
/**
* The {@link android.Manifest.permission#READ_CALENDAR} AppSearch supported in {@link
* SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
*/
public static final int READ_CALENDAR = 2;
/**
* The {@link android.Manifest.permission#READ_CONTACTS} AppSearch supported in {@link
* SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
*/
public static final int READ_CONTACTS = 3;
/**
* The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} AppSearch supported in {@link
* SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
*/
public static final int READ_EXTERNAL_STORAGE = 4;
/**
* The {@link android.Manifest.permission#READ_HOME_APP_SEARCH_DATA} AppSearch supported in
* {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
*/
public static final int READ_HOME_APP_SEARCH_DATA = 5;
/**
* The {@link android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA} AppSearch supported in
* {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
*/
public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6;
private final Set<AppSearchSchema> mSchemas;
private final Set<String> mSchemasNotDisplayedBySystem;
private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
private final Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
private final Map<String, Migrator> mMigrators;
private final boolean mForceOverride;
private final int mVersion;
SetSchemaRequest(
@NonNull Set<AppSearchSchema> schemas,
@NonNull Set<String> schemasNotDisplayedBySystem,
@NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
@NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions,
@NonNull Map<String, Migrator> migrators,
boolean forceOverride,
int version) {
mSchemas = Objects.requireNonNull(schemas);
mSchemasNotDisplayedBySystem = Objects.requireNonNull(schemasNotDisplayedBySystem);
mSchemasVisibleToPackages = Objects.requireNonNull(schemasVisibleToPackages);
mSchemasVisibleToPermissions = Objects.requireNonNull(schemasVisibleToPermissions);
mMigrators = Objects.requireNonNull(migrators);
mForceOverride = forceOverride;
mVersion = version;
}
/** Returns the {@link AppSearchSchema} types that are part of this request. */
@NonNull
public Set<AppSearchSchema> getSchemas() {
return Collections.unmodifiableSet(mSchemas);
}
/**
* Returns all the schema types that are opted out of being displayed and visible on any system
* UI surface.
*/
@NonNull
public Set<String> getSchemasNotDisplayedBySystem() {
return Collections.unmodifiableSet(mSchemasNotDisplayedBySystem);
}
/**
* Returns a mapping of schema types to the set of packages that have access to that schema
* type.
*
* <p>It’s inefficient to call this method repeatedly.
*/
@NonNull
public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
for (Map.Entry<String, Set<PackageIdentifier>> entry :
mSchemasVisibleToPackages.entrySet()) {
copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
}
return copy;
}
/**
* Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
* combinations that querier must hold to access that schema type.
*
* <p>The querier could read the {@link GenericDocument} objects under the {@code schemaType} if
* they holds ALL required permissions of ANY of the individual value sets.
*
* <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
* {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
*
* <ul>
* <li>A querier holds both PermissionA and PermissionB has access.
* <li>A querier holds both PermissionC and PermissionD has access.
* <li>A querier holds only PermissionE has access.
* <li>A querier holds both PermissionA and PermissionE has access.
* <li>A querier holds only PermissionA doesn't have access.
* <li>A querier holds both PermissionA and PermissionC doesn't have access.
* </ul>
*
* <p>It’s inefficient to call this method repeatedly.
*
* @return The map contains schema type and all combinations of required permission for querier
* to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS}, {@link
* SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS}, {@link
* SetSchemaRequest#READ_EXTERNAL_STORAGE}, {@link
* SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and {@link
* SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
*/
@NonNull
public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
return deepCopy(mSchemasVisibleToPermissions);
}
/**
* Returns the map of {@link Migrator}, the key will be the schema type of the {@link Migrator}
* associated with.
*/
@NonNull
public Map<String, Migrator> getMigrators() {
return Collections.unmodifiableMap(mMigrators);
}
/**
* Returns a mapping of {@link AppSearchSchema} types to the set of packages that have access to
* that schema type.
*
* <p>A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
* modifiable map. This is not meant to be unhidden and should only be used by internal classes.
*
* @hide
*/
@NonNull
public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackagesInternal() {
return mSchemasVisibleToPackages;
}
/** Returns whether this request will force the schema to be overridden. */
public boolean isForceOverride() {
return mForceOverride;
}
/** Returns the database overall schema version. */
@IntRange(from = 1)
public int getVersion() {
return mVersion;
}
/** Builder for {@link SetSchemaRequest} objects. */
public static final class Builder {
private static final int DEFAULT_VERSION = 1;
private ArraySet<AppSearchSchema> mSchemas = new ArraySet<>();
private ArraySet<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
new ArrayMap<>();
private ArrayMap<String, Set<Set<Integer>>> mSchemasVisibleToPermissions = new ArrayMap<>();
private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
private boolean mForceOverride = false;
private int mVersion = DEFAULT_VERSION;
private boolean mBuilt = false;
/**
* Adds one or more {@link AppSearchSchema} types to the schema.
*
* <p>An {@link AppSearchSchema} object represents one type of structured data.
*
* <p>Any documents of these types will be displayed on system UI surfaces by default.
*/
@CanIgnoreReturnValue
@NonNull
public Builder addSchemas(@NonNull AppSearchSchema... schemas) {
Objects.requireNonNull(schemas);
resetIfBuilt();
return addSchemas(Arrays.asList(schemas));
}
/**
* Adds a collection of {@link AppSearchSchema} objects to the schema.
*
* <p>An {@link AppSearchSchema} object represents one type of structured data.
*/
@CanIgnoreReturnValue
@NonNull
public Builder addSchemas(@NonNull Collection<AppSearchSchema> schemas) {
Objects.requireNonNull(schemas);
resetIfBuilt();
mSchemas.addAll(schemas);
return this;
}
/**
* Sets whether or not documents from the provided {@code schemaType} will be displayed and
* visible on any system UI surface.
*
* <p>This setting applies to the provided {@code schemaType} only, and does not persist
* across {@link AppSearchSession#setSchema} calls.
*
* <p>The default behavior, if this method is not called, is to allow types to be displayed
* on system UI surfaces.
*
* @param schemaType The name of an {@link AppSearchSchema} within the same {@link
* SetSchemaRequest}, which will be configured.
* @param displayed Whether documents of this type will be displayed on system UI surfaces.
*/
// Merged list available from getSchemasNotDisplayedBySystem
@CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setSchemaTypeDisplayedBySystem(
@NonNull String schemaType, boolean displayed) {
Objects.requireNonNull(schemaType);
resetIfBuilt();
if (displayed) {
mSchemasNotDisplayedBySystem.remove(schemaType);
} else {
mSchemasNotDisplayedBySystem.add(schemaType);
}
return this;
}
/**
* Adds a set of required Android {@link android.Manifest.permission} combination to the
* given schema type.
*
* <p>If the querier holds ALL of the required permissions in this combination, they will
* have access to read {@link GenericDocument} objects of the given schema type.
*
* <p>You can call this method to add multiple permission combinations, and the querier will
* have access if they holds ANY of the combinations.
*
* <p>The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR}, {@link
* #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE}, {@link #READ_HOME_APP_SEARCH_DATA} and
* {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
*
* @see android.Manifest.permission#READ_SMS
* @see android.Manifest.permission#READ_CALENDAR
* @see android.Manifest.permission#READ_CONTACTS
* @see android.Manifest.permission#READ_EXTERNAL_STORAGE
* @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
* @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
* @param schemaType The schema type to set visibility on.
* @param permissions A set of required Android permissions the caller need to hold to
* access {@link GenericDocument} objects that under the given schema.
* @throws IllegalArgumentException – if input unsupported permission.
*/
// Merged list available from getRequiredPermissionsForSchemaTypeVisibility
@CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder addRequiredPermissionsForSchemaTypeVisibility(
@NonNull String schemaType,
@AppSearchSupportedPermission @NonNull Set<Integer> permissions) {
Objects.requireNonNull(schemaType);
Objects.requireNonNull(permissions);
for (int permission : permissions) {
Preconditions.checkArgumentInRange(
permission, READ_SMS, READ_ASSISTANT_APP_SEARCH_DATA, "permission");
}
resetIfBuilt();
Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
if (visibleToPermissions == null) {
visibleToPermissions = new ArraySet<>();
mSchemasVisibleToPermissions.put(schemaType, visibleToPermissions);
}
visibleToPermissions.add(permissions);
return this;
}
/** Clears all required permissions combinations for the given schema type. */
@NonNull
public Builder clearRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType) {
Objects.requireNonNull(schemaType);
resetIfBuilt();
mSchemasVisibleToPermissions.remove(schemaType);
return this;
}
/**
* Sets whether or not documents from the provided {@code schemaType} can be read by the
* specified package.
*
* <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
* and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
*
* <p>To opt into one-way data sharing with another application, the developer will need to
* explicitly grant the other application’s package name and certificate Read access to its
* data.
*
* <p>For two-way data sharing, both applications need to explicitly grant Read access to
* one another.
*
* <p>By default, data sharing between applications is disabled.
*
* @param schemaType The schema type to set visibility on.
* @param visible Whether the {@code schemaType} will be visible or not.
* @param packageIdentifier Represents the package that will be granted visibility.
*/
// Merged list available from getSchemasVisibleToPackages
@CanIgnoreReturnValue
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setSchemaTypeVisibilityForPackage(
@NonNull String schemaType,
boolean visible,
@NonNull PackageIdentifier packageIdentifier) {
Objects.requireNonNull(schemaType);
Objects.requireNonNull(packageIdentifier);
resetIfBuilt();
Set<PackageIdentifier> packageIdentifiers = mSchemasVisibleToPackages.get(schemaType);
if (visible) {
if (packageIdentifiers == null) {
packageIdentifiers = new ArraySet<>();
}
packageIdentifiers.add(packageIdentifier);
mSchemasVisibleToPackages.put(schemaType, packageIdentifiers);
} else {
if (packageIdentifiers == null) {
// Return early since there was nothing set to begin with.
return this;
}
packageIdentifiers.remove(packageIdentifier);
if (packageIdentifiers.isEmpty()) {
// Remove the entire key so that we don't have empty sets as values.
mSchemasVisibleToPackages.remove(schemaType);
}
}
return this;
}
/**
* Sets the {@link Migrator} associated with the given SchemaType.
*
* <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
* from the current version number stored in AppSearch to the final version set via {@link
* #setVersion}.
*
* <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
* is different from the final version set via {@link #setVersion} and {@link
* Migrator#shouldMigrate} returns {@code true}.
*
* <p>The target schema type of the output {@link GenericDocument} of {@link
* Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
* SetSchemaRequest}.
*
* @param schemaType The schema type to set migrator on.
* @param migrator The migrator translates a document from its current version to the final
* version set via {@link #setVersion}.
* @see SetSchemaRequest.Builder#setVersion
* @see SetSchemaRequest.Builder#addSchemas
* @see AppSearchSession#setSchema
*/
@CanIgnoreReturnValue
@NonNull
@SuppressLint("MissingGetterMatchingBuilder") // Getter return plural objects.
public Builder setMigrator(@NonNull String schemaType, @NonNull Migrator migrator) {
Objects.requireNonNull(schemaType);
Objects.requireNonNull(migrator);
resetIfBuilt();
mMigrators.put(schemaType, migrator);
return this;
}
/**
* Sets a Map of {@link Migrator}s.
*
* <p>The key of the map is the schema type that the {@link Migrator} value applies to.
*
* <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
* from the current version number stored in AppSearch to the final version set via {@link
* #setVersion}.
*
* <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
* is different from the final version set via {@link #setVersion} and {@link
* Migrator#shouldMigrate} returns {@code true}.
*
* <p>The target schema type of the output {@link GenericDocument} of {@link
* Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
* SetSchemaRequest}.
*
* @param migrators A {@link Map} of migrators that translate a document from its current
* version to the final version set via {@link #setVersion}. The key of the map is the
* schema type that the {@link Migrator} value applies to.
* @see SetSchemaRequest.Builder#setVersion
* @see SetSchemaRequest.Builder#addSchemas
* @see AppSearchSession#setSchema
*/
@CanIgnoreReturnValue
@NonNull
public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
Objects.requireNonNull(migrators);
resetIfBuilt();
mMigrators.putAll(migrators);
return this;
}
/**
* Sets whether or not to override the current schema in the {@link AppSearchSession}
* database.
*
* <p>Call this method whenever backward incompatible changes need to be made by setting
* {@code forceOverride} to {@code true}. As a result, during execution of the setSchema
* operation, all documents that are incompatible with the new schema will be deleted and
* the new schema will be saved and persisted.
*
* <p>By default, this is {@code false}.
*/
@CanIgnoreReturnValue
@NonNull
public Builder setForceOverride(boolean forceOverride) {
resetIfBuilt();
mForceOverride = forceOverride;
return this;
}
/**
* Sets the version number of the overall {@link AppSearchSchema} in the database.
*
* <p>The {@link AppSearchSession} database can only ever hold documents for one version at
* a time.
*
* <p>Setting a version number that is different from the version number currently stored in
* AppSearch will result in AppSearch calling the {@link Migrator}s provided to {@link
* AppSearchSession#setSchema} to migrate the documents already in AppSearch from the
* previous version to the one set in this request. The version number can be updated
* without any other changes to the set of schemas.
*
* <p>The version number can stay the same, increase, or decrease relative to the current
* version number that is already stored in the {@link AppSearchSession} database.
*
* <p>The version of an empty database will always be 0. You cannot set version to the
* {@link SetSchemaRequest}, if it doesn't contains any {@link AppSearchSchema}.
*
* @param version A positive integer representing the version of the entire set of schemas
* represents the version of the whole schema in the {@link AppSearchSession} database,
* default version is 1.
* @throws IllegalArgumentException if the version is negative.
* @see AppSearchSession#setSchema
* @see Migrator
* @see SetSchemaRequest.Builder#setMigrator
*/
@CanIgnoreReturnValue
@NonNull
public Builder setVersion(@IntRange(from = 1) int version) {
Preconditions.checkArgument(version >= 1, "Version must be a positive number.");
resetIfBuilt();
mVersion = version;
return this;
}
/**
* Builds a new {@link SetSchemaRequest} object.
*
* @throws IllegalArgumentException if schema types were referenced, but the corresponding
* {@link AppSearchSchema} type was never added.
*/
@NonNull
public SetSchemaRequest build() {
// Verify that any schema types with display or visibility settings refer to a real
// schema.
// Create a copy because we're going to remove from the set for verification purposes.
Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());
for (AppSearchSchema schema : mSchemas) {
referencedSchemas.remove(schema.getSchemaType());
}
if (!referencedSchemas.isEmpty()) {
// We still have schema types that weren't seen in our mSchemas set. This means
// there wasn't a corresponding AppSearchSchema.
throw new IllegalArgumentException(
"Schema types " + referencedSchemas + " referenced, but were not added.");
}
if (mSchemas.isEmpty() && mVersion != DEFAULT_VERSION) {
throw new IllegalArgumentException(
"Cannot set version to the request if schema is empty.");
}
mBuilt = true;
return new SetSchemaRequest(
mSchemas,
mSchemasNotDisplayedBySystem,
mSchemasVisibleToPackages,
mSchemasVisibleToPermissions,
mMigrators,
mForceOverride,
mVersion);
}
private void resetIfBuilt() {
if (mBuilt) {
ArrayMap<String, Set<PackageIdentifier>> schemasVisibleToPackages =
new ArrayMap<>(mSchemasVisibleToPackages.size());
for (Map.Entry<String, Set<PackageIdentifier>> entry :
mSchemasVisibleToPackages.entrySet()) {
schemasVisibleToPackages.put(entry.getKey(), new ArraySet<>(entry.getValue()));
}
mSchemasVisibleToPackages = schemasVisibleToPackages;
mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);
mSchemas = new ArraySet<>(mSchemas);
mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
mMigrators = new ArrayMap<>(mMigrators);
mBuilt = false;
}
}
}
static ArrayMap<String, Set<Set<Integer>>> deepCopy(
@NonNull Map<String, Set<Set<Integer>>> original) {
ArrayMap<String, Set<Set<Integer>>> copy = new ArrayMap<>(original.size());
for (Map.Entry<String, Set<Set<Integer>>> entry : original.entrySet()) {
Set<Set<Integer>> valueCopy = new ArraySet<>();
for (Set<Integer> innerValue : entry.getValue()) {
valueCopy.add(new ArraySet<>(innerValue));
}
copy.put(entry.getKey(), valueCopy);
}
return copy;
}
}