/* * 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.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.appsearch.annotation.CanIgnoreReturnValue; import android.util.ArrayMap; import android.util.ArraySet; import com.android.appsearch.flags.Flags; 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. * *

The schema is composed of a collection of {@link AppSearchSchema} objects, each of which * defines a unique type of data. * *

The first call to SetSchemaRequest will set the provided schema and store it within the {@link * AppSearchSession} database. * *

Subsequent calls will compare the provided schema to the previously saved schema, to determine * how to treat existing documents. * *

The following types of schema modifications are always safe and are made without deleting any * existing documents: * *

* *

The following types of schema changes are not backwards compatible: * *

* *

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. * *

Backward incompatible changes can be made by : * *

* * @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, ENTERPRISE_ACCESS, MANAGED_PROFILE_CONTACTS_ACCESS, }) @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; /** * A schema must have this permission set through {@link * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} to be visible to an * {@link EnterpriseGlobalSearchSession}. A call from a regular {@link GlobalSearchSession} will * not count as having this permission. * * @hide */ public static final int ENTERPRISE_ACCESS = 7; /** * A schema with this permission set through {@link * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} requires the caller * to have managed profile contacts access from {@link android.app.admin.DevicePolicyManager} to * be visible. This permission indicates that the protected schema may expose managed profile * data for contacts search. * * @hide */ public static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8; private final Set mSchemas; private final Set mSchemasNotDisplayedBySystem; private final Map> mSchemasVisibleToPackages; private final Map>> mSchemasVisibleToPermissions; private final Map mPubliclyVisibleSchemas; private final Map> mSchemasVisibleToConfigs; private final Map mMigrators; private final boolean mForceOverride; private final int mVersion; SetSchemaRequest( @NonNull Set schemas, @NonNull Set schemasNotDisplayedBySystem, @NonNull Map> schemasVisibleToPackages, @NonNull Map>> schemasVisibleToPermissions, @NonNull Map publiclyVisibleSchemas, @NonNull Map> schemasVisibleToConfigs, @NonNull Map migrators, boolean forceOverride, int version) { mSchemas = Objects.requireNonNull(schemas); mSchemasNotDisplayedBySystem = Objects.requireNonNull(schemasNotDisplayedBySystem); mSchemasVisibleToPackages = Objects.requireNonNull(schemasVisibleToPackages); mSchemasVisibleToPermissions = Objects.requireNonNull(schemasVisibleToPermissions); mPubliclyVisibleSchemas = Objects.requireNonNull(publiclyVisibleSchemas); mSchemasVisibleToConfigs = Objects.requireNonNull(schemasVisibleToConfigs); mMigrators = Objects.requireNonNull(migrators); mForceOverride = forceOverride; mVersion = version; } /** Returns the {@link AppSearchSchema} types that are part of this request. */ @NonNull public Set 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 getSchemasNotDisplayedBySystem() { return Collections.unmodifiableSet(mSchemasNotDisplayedBySystem); } /** * Returns a mapping of schema types to the set of packages that have access to that schema * type. * *

It’s inefficient to call this method repeatedly. */ @NonNull public Map> getSchemasVisibleToPackages() { Map> copy = new ArrayMap<>(); for (Map.Entry> 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. * *

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. * *

For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB}, * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}. * *

    *
  • A querier holds both PermissionA and PermissionB has access. *
  • A querier holds both PermissionC and PermissionD has access. *
  • A querier holds only PermissionE has access. *
  • A querier holds both PermissionA and PermissionE has access. *
  • A querier holds only PermissionA doesn't have access. *
  • A querier holds both PermissionA and PermissionC doesn't have access. *
* *

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}. */ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden @NonNull public Map>> getRequiredPermissionsForSchemaTypeVisibility() { return deepCopy(mSchemasVisibleToPermissions); } /** * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying the * package the schemas are from. */ @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA) @NonNull public Map getPubliclyVisibleSchemas() { return Collections.unmodifiableMap(mPubliclyVisibleSchemas); } /** * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have * access to that schema type. * *

It’s inefficient to call this method repeatedly. * * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig */ @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) @NonNull public Map> getSchemasVisibleToConfigs() { Map> copy = new ArrayMap<>(); for (Map.Entry> entry : mSchemasVisibleToConfigs.entrySet()) { copy.put(entry.getKey(), new ArraySet<>(entry.getValue())); } return copy; } /** * Returns the map of {@link Migrator}, the key will be the schema type of the {@link Migrator} * associated with. */ @NonNull public Map getMigrators() { return Collections.unmodifiableMap(mMigrators); } /** * Returns a mapping of {@link AppSearchSchema} types to the set of packages that have access to * that schema type. * *

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> 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 mSchemas = new ArraySet<>(); private ArraySet mSchemasNotDisplayedBySystem = new ArraySet<>(); private ArrayMap> mSchemasVisibleToPackages = new ArrayMap<>(); private ArrayMap>> mSchemasVisibleToPermissions = new ArrayMap<>(); private ArrayMap mPubliclyVisibleSchemas = new ArrayMap<>(); private ArrayMap> mSchemaVisibleToConfigs = new ArrayMap<>(); private ArrayMap 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. * *

An {@link AppSearchSchema} object represents one type of structured data. * *

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. * *

An {@link AppSearchSchema} object represents one type of structured data. */ @CanIgnoreReturnValue @NonNull public Builder addSchemas(@NonNull Collection 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. * *

This setting applies to the provided {@code schemaType} only, and does not persist * across {@link AppSearchSession#setSchema} calls. * *

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. * *

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. * *

You can call this method to add multiple permission combinations, and the querier will * have access if they holds ANY of the combinations. * *

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}. * *

The relationship between permissions added in this method and package visibility * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access the * schema if they match ANY requirements. If you want to set "AND" requirements like a * caller must hold required permissions AND it is a specified package, please use {@link * #addSchemaTypeVisibleToConfig}. * * @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. */ // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden // Merged list available from getRequiredPermissionsForSchemaTypeVisibility @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull public Builder addRequiredPermissionsForSchemaTypeVisibility( @NonNull String schemaType, @AppSearchSupportedPermission @NonNull Set permissions) { Objects.requireNonNull(schemaType); Objects.requireNonNull(permissions); for (int permission : permissions) { Preconditions.checkArgumentInRange( permission, READ_SMS, MANAGED_PROFILE_CONTACTS_ACCESS, "permission"); } resetIfBuilt(); Set> 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. */ @CanIgnoreReturnValue @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. * *

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}. * *

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. * *

For two-way data sharing, both applications need to explicitly grant Read access to * one another. * *

By default, data sharing between applications is disabled. * *

The relationship between permissions added in this method and package visibility * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access the * schema if they match ANY requirements. If you want to set "AND" requirements like a * caller must hold required permissions AND it is a specified package, please use {@link * #addSchemaTypeVisibleToConfig}. * * @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 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; } /** * Specify that the schema should be publicly available, to packages which already have * visibility to {@code packageIdentifier}. This visibility is determined by the result of * {@link android.content.pm.PackageManager#canPackageQuery}. * *

It is possible for the packageIdentifier parameter to be different from the package * performing the indexing. This might happen in the case of an on-device indexer processing * information about various packages. The visibility will be the same regardless of which * package indexes the document, as the visibility is based on the packageIdentifier * parameter. * *

If this is called repeatedly with the same schema, the {@link PackageIdentifier} in * the last call will be used as the "from" package for that schema. * *

Calling this with packageIdentifier set to null is valid, and will remove public * visibility for the schema. * * @param schema the schema to make publicly accessible. * @param packageIdentifier if an app can see this package via * PackageManager#canPackageQuery, it will be able to see the documents of type {@code * schema}. */ // Merged list available from getPubliclyVisibleSchemas @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA) @NonNull public Builder setPubliclyVisibleSchema( @NonNull String schema, @Nullable PackageIdentifier packageIdentifier) { Objects.requireNonNull(schema); resetIfBuilt(); // If the package identifier is null or empty we clear public visibility if (packageIdentifier == null || packageIdentifier.getPackageName().isEmpty()) { mPubliclyVisibleSchemas.remove(schema); return this; } mPubliclyVisibleSchemas.put(schema, packageIdentifier); return this; } /** * Sets the documents from the provided {@code schemaType} can be read by the caller if they * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}. * *

The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A caller * must match ALL requirements to access the schema. For example, a caller must hold * required permissions AND it is a specified package. * *

You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s, * and the querier will have access if they match ANY of the {@link SchemaVisibilityConfig}. * * @param schemaType The schema type to set visibility on. * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all requirements * that a call must to match to access the schema. */ // Merged list available from getSchemasVisibleToConfigs @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) @NonNull public Builder addSchemaTypeVisibleToConfig( @NonNull String schemaType, @NonNull SchemaVisibilityConfig schemaVisibilityConfig) { Objects.requireNonNull(schemaType); Objects.requireNonNull(schemaVisibilityConfig); resetIfBuilt(); Set visibleToConfigs = mSchemaVisibleToConfigs.get(schemaType); if (visibleToConfigs == null) { visibleToConfigs = new ArraySet<>(); mSchemaVisibleToConfigs.put(schemaType, visibleToConfigs); } visibleToConfigs.add(schemaVisibilityConfig); return this; } /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */ @CanIgnoreReturnValue @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) @NonNull public Builder clearSchemaTypeVisibleToConfigs(@NonNull String schemaType) { Objects.requireNonNull(schemaType); resetIfBuilt(); mSchemaVisibleToConfigs.remove(schemaType); return this; } /** * Sets the {@link Migrator} associated with the given SchemaType. * *

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}. * *

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}. * *

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. * *

The key of the map is the schema type that the {@link Migrator} value applies to. * *

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}. * *

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}. * *

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 migrators) { Objects.requireNonNull(migrators); resetIfBuilt(); mMigrators.putAll(migrators); return this; } /** * Sets whether or not to override the current schema in the {@link AppSearchSession} * database. * *

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. * *

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. * *

The {@link AppSearchSession} database can only ever hold documents for one version at * a time. * *

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. * *

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. * *

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 referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem); referencedSchemas.addAll(mSchemasVisibleToPackages.keySet()); referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet()); referencedSchemas.addAll(mPubliclyVisibleSchemas.keySet()); referencedSchemas.addAll(mSchemaVisibleToConfigs.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, mPubliclyVisibleSchemas, mSchemaVisibleToConfigs, mMigrators, mForceOverride, mVersion); } private void resetIfBuilt() { if (mBuilt) { ArrayMap> schemasVisibleToPackages = new ArrayMap<>(mSchemasVisibleToPackages.size()); for (Map.Entry> entry : mSchemasVisibleToPackages.entrySet()) { schemasVisibleToPackages.put(entry.getKey(), new ArraySet<>(entry.getValue())); } mSchemasVisibleToPackages = schemasVisibleToPackages; mPubliclyVisibleSchemas = new ArrayMap<>(mPubliclyVisibleSchemas); mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions); ArrayMap> schemaVisibleToConfigs = new ArrayMap<>(mSchemaVisibleToConfigs.size()); for (Map.Entry> entry : mSchemaVisibleToConfigs.entrySet()) { schemaVisibleToConfigs.put(entry.getKey(), new ArraySet<>(entry.getValue())); } mSchemaVisibleToConfigs = schemaVisibleToConfigs; mSchemas = new ArraySet<>(mSchemas); mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem); mMigrators = new ArrayMap<>(mMigrators); mBuilt = false; } } } private static ArrayMap>> deepCopy( @NonNull Map>> original) { ArrayMap>> copy = new ArrayMap<>(original.size()); for (Map.Entry>> entry : original.entrySet()) { Set> valueCopy = new ArraySet<>(); for (Set innerValue : entry.getValue()) { valueCopy.add(new ArraySet<>(innerValue)); } copy.put(entry.getKey(), valueCopy); } return copy; } }