• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 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 android.app.appsearch;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.annotation.SuppressLint;
22 import android.util.ArrayMap;
23 import android.util.ArraySet;
24 
25 import com.android.internal.util.Preconditions;
26 
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.Map;
31 import java.util.Objects;
32 import java.util.Set;
33 
34 /**
35  * Encapsulates a request to update the schema of an {@link AppSearchSession} database.
36  *
37  * <p>The schema is composed of a collection of {@link AppSearchSchema} objects, each of which
38  * defines a unique type of data.
39  *
40  * <p>The first call to SetSchemaRequest will set the provided schema and store it within the {@link
41  * AppSearchSession} database.
42  *
43  * <p>Subsequent calls will compare the provided schema to the previously saved schema, to determine
44  * how to treat existing documents.
45  *
46  * <p>The following types of schema modifications are always safe and are made without deleting any
47  * existing documents:
48  *
49  * <ul>
50  *   <li>Addition of new {@link AppSearchSchema} types
51  *   <li>Addition of new properties to an existing {@link AppSearchSchema} type
52  *   <li>Changing the cardinality of a property to be less restrictive
53  * </ul>
54  *
55  * <p>The following types of schema changes are not backwards compatible:
56  *
57  * <ul>
58  *   <li>Removal of an existing {@link AppSearchSchema} type
59  *   <li>Removal of a property from an existing {@link AppSearchSchema} type
60  *   <li>Changing the data type of an existing property
61  *   <li>Changing the cardinality of a property to be more restrictive
62  * </ul>
63  *
64  * <p>Providing a schema with incompatible changes, will throw an {@link
65  * android.app.appsearch.exceptions.AppSearchException}, with a message describing the
66  * incompatibility. As a result, the previously set schema will remain unchanged.
67  *
68  * <p>Backward incompatible changes can be made by :
69  *
70  * <ul>
71  *   <li>setting {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}. This
72  *       deletes all documents that are incompatible with the new schema. The new schema is then
73  *       saved and persisted to disk.
74  *   <li>Add a {@link Migrator} for each incompatible type and make no deletion. The migrator will
75  *       migrate documents from it's old schema version to the new version. Migrated types will be
76  *       set into both {@link SetSchemaResponse#getIncompatibleTypes()} and {@link
77  *       SetSchemaResponse#getMigratedTypes()}. See the migration section below.
78  * </ul>
79  *
80  * @see AppSearchSession#setSchema
81  * @see Migrator
82  */
83 public final class SetSchemaRequest {
84     private final Set<AppSearchSchema> mSchemas;
85     private final Set<String> mSchemasNotDisplayedBySystem;
86     private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
87     private final Map<String, Migrator> mMigrators;
88     private final boolean mForceOverride;
89     private final int mVersion;
90 
SetSchemaRequest( @onNull Set<AppSearchSchema> schemas, @NonNull Set<String> schemasNotDisplayedBySystem, @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages, @NonNull Map<String, Migrator> migrators, boolean forceOverride, int version)91     SetSchemaRequest(
92             @NonNull Set<AppSearchSchema> schemas,
93             @NonNull Set<String> schemasNotDisplayedBySystem,
94             @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
95             @NonNull Map<String, Migrator> migrators,
96             boolean forceOverride,
97             int version) {
98         mSchemas = Objects.requireNonNull(schemas);
99         mSchemasNotDisplayedBySystem = Objects.requireNonNull(schemasNotDisplayedBySystem);
100         mSchemasVisibleToPackages = Objects.requireNonNull(schemasVisibleToPackages);
101         mMigrators = Objects.requireNonNull(migrators);
102         mForceOverride = forceOverride;
103         mVersion = version;
104     }
105 
106     /** Returns the {@link AppSearchSchema} types that are part of this request. */
107     @NonNull
getSchemas()108     public Set<AppSearchSchema> getSchemas() {
109         return Collections.unmodifiableSet(mSchemas);
110     }
111 
112     /**
113      * Returns all the schema types that are opted out of being displayed and visible on any system
114      * UI surface.
115      */
116     @NonNull
getSchemasNotDisplayedBySystem()117     public Set<String> getSchemasNotDisplayedBySystem() {
118         return Collections.unmodifiableSet(mSchemasNotDisplayedBySystem);
119     }
120 
121     /**
122      * Returns a mapping of schema types to the set of packages that have access to that schema
123      * type.
124      *
125      * <p>It’s inefficient to call this method repeatedly.
126      */
127     @NonNull
getSchemasVisibleToPackages()128     public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
129         Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
130         for (String key : mSchemasVisibleToPackages.keySet()) {
131             copy.put(key, new ArraySet<>(mSchemasVisibleToPackages.get(key)));
132         }
133         return copy;
134     }
135 
136     /**
137      * Returns the map of {@link Migrator}, the key will be the schema type of the {@link Migrator}
138      * associated with.
139      */
140     @NonNull
getMigrators()141     public Map<String, Migrator> getMigrators() {
142         return Collections.unmodifiableMap(mMigrators);
143     }
144 
145     /**
146      * Returns a mapping of {@link AppSearchSchema} types to the set of packages that have access to
147      * that schema type.
148      *
149      * <p>A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
150      * modifiable map. This is not meant to be unhidden and should only be used by internal classes.
151      *
152      * @hide
153      */
154     @NonNull
getSchemasVisibleToPackagesInternal()155     public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackagesInternal() {
156         return mSchemasVisibleToPackages;
157     }
158 
159     /** Returns whether this request will force the schema to be overridden. */
isForceOverride()160     public boolean isForceOverride() {
161         return mForceOverride;
162     }
163 
164     /** Returns the database overall schema version. */
165     @IntRange(from = 1)
getVersion()166     public int getVersion() {
167         return mVersion;
168     }
169 
170     /** Builder for {@link SetSchemaRequest} objects. */
171     public static final class Builder {
172         private static final int DEFAULT_VERSION = 1;
173         private ArraySet<AppSearchSchema> mSchemas = new ArraySet<>();
174         private ArraySet<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
175         private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
176                 new ArrayMap<>();
177         private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
178         private boolean mForceOverride = false;
179         private int mVersion = DEFAULT_VERSION;
180         private boolean mBuilt = false;
181 
182         /**
183          * Adds one or more {@link AppSearchSchema} types to the schema.
184          *
185          * <p>An {@link AppSearchSchema} object represents one type of structured data.
186          *
187          * <p>Any documents of these types will be displayed on system UI surfaces by default.
188          */
189         @NonNull
addSchemas(@onNull AppSearchSchema... schemas)190         public Builder addSchemas(@NonNull AppSearchSchema... schemas) {
191             Objects.requireNonNull(schemas);
192             resetIfBuilt();
193             return addSchemas(Arrays.asList(schemas));
194         }
195 
196         /**
197          * Adds a collection of {@link AppSearchSchema} objects to the schema.
198          *
199          * <p>An {@link AppSearchSchema} object represents one type of structured data.
200          */
201         @NonNull
addSchemas(@onNull Collection<AppSearchSchema> schemas)202         public Builder addSchemas(@NonNull Collection<AppSearchSchema> schemas) {
203             Objects.requireNonNull(schemas);
204             resetIfBuilt();
205             mSchemas.addAll(schemas);
206             return this;
207         }
208 
209         /**
210          * Sets whether or not documents from the provided {@code schemaType} will be displayed and
211          * visible on any system UI surface.
212          *
213          * <p>This setting applies to the provided {@code schemaType} only, and does not persist
214          * across {@link AppSearchSession#setSchema} calls.
215          *
216          * <p>The default behavior, if this method is not called, is to allow types to be displayed
217          * on system UI surfaces.
218          *
219          * @param schemaType The name of an {@link AppSearchSchema} within the same {@link
220          *     SetSchemaRequest}, which will be configured.
221          * @param displayed Whether documents of this type will be displayed on system UI surfaces.
222          */
223         // Merged list available from getSchemasNotDisplayedBySystem
224         @SuppressLint("MissingGetterMatchingBuilder")
225         @NonNull
setSchemaTypeDisplayedBySystem( @onNull String schemaType, boolean displayed)226         public Builder setSchemaTypeDisplayedBySystem(
227                 @NonNull String schemaType, boolean displayed) {
228             Objects.requireNonNull(schemaType);
229             resetIfBuilt();
230             if (displayed) {
231                 mSchemasNotDisplayedBySystem.remove(schemaType);
232             } else {
233                 mSchemasNotDisplayedBySystem.add(schemaType);
234             }
235             return this;
236         }
237 
238         /**
239          * Sets whether or not documents from the provided {@code schemaType} can be read by the
240          * specified package.
241          *
242          * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
243          * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
244          *
245          * <p>To opt into one-way data sharing with another application, the developer will need to
246          * explicitly grant the other application’s package name and certificate Read access to its
247          * data.
248          *
249          * <p>For two-way data sharing, both applications need to explicitly grant Read access to
250          * one another.
251          *
252          * <p>By default, data sharing between applications is disabled.
253          *
254          * @param schemaType The schema type to set visibility on.
255          * @param visible Whether the {@code schemaType} will be visible or not.
256          * @param packageIdentifier Represents the package that will be granted visibility.
257          */
258         // Merged list available from getSchemasVisibleToPackages
259         @SuppressLint("MissingGetterMatchingBuilder")
260         @NonNull
setSchemaTypeVisibilityForPackage( @onNull String schemaType, boolean visible, @NonNull PackageIdentifier packageIdentifier)261         public Builder setSchemaTypeVisibilityForPackage(
262                 @NonNull String schemaType,
263                 boolean visible,
264                 @NonNull PackageIdentifier packageIdentifier) {
265             Objects.requireNonNull(schemaType);
266             Objects.requireNonNull(packageIdentifier);
267             resetIfBuilt();
268 
269             Set<PackageIdentifier> packageIdentifiers = mSchemasVisibleToPackages.get(schemaType);
270             if (visible) {
271                 if (packageIdentifiers == null) {
272                     packageIdentifiers = new ArraySet<>();
273                 }
274                 packageIdentifiers.add(packageIdentifier);
275                 mSchemasVisibleToPackages.put(schemaType, packageIdentifiers);
276             } else {
277                 if (packageIdentifiers == null) {
278                     // Return early since there was nothing set to begin with.
279                     return this;
280                 }
281                 packageIdentifiers.remove(packageIdentifier);
282                 if (packageIdentifiers.isEmpty()) {
283                     // Remove the entire key so that we don't have empty sets as values.
284                     mSchemasVisibleToPackages.remove(schemaType);
285                 }
286             }
287 
288             return this;
289         }
290 
291         /**
292          * Sets the {@link Migrator} associated with the given SchemaType.
293          *
294          * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
295          * from the current version number stored in AppSearch to the final version set via {@link
296          * #setVersion}.
297          *
298          * <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
299          * is different from the final version set via {@link #setVersion} and {@link
300          * Migrator#shouldMigrate} returns {@code true}.
301          *
302          * <p>The target schema type of the output {@link GenericDocument} of {@link
303          * Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
304          * SetSchemaRequest}.
305          *
306          * @param schemaType The schema type to set migrator on.
307          * @param migrator The migrator translates a document from its current version to the final
308          *     version set via {@link #setVersion}.
309          * @see SetSchemaRequest.Builder#setVersion
310          * @see SetSchemaRequest.Builder#addSchemas
311          * @see AppSearchSession#setSchema
312          */
313         @NonNull
314         @SuppressLint("MissingGetterMatchingBuilder") // Getter return plural objects.
setMigrator(@onNull String schemaType, @NonNull Migrator migrator)315         public Builder setMigrator(@NonNull String schemaType, @NonNull Migrator migrator) {
316             Objects.requireNonNull(schemaType);
317             Objects.requireNonNull(migrator);
318             resetIfBuilt();
319             mMigrators.put(schemaType, migrator);
320             return this;
321         }
322 
323         /**
324          * Sets a Map of {@link Migrator}s.
325          *
326          * <p>The key of the map is the schema type that the {@link Migrator} value applies to.
327          *
328          * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
329          * from the current version number stored in AppSearch to the final version set via {@link
330          * #setVersion}.
331          *
332          * <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
333          * is different from the final version set via {@link #setVersion} and {@link
334          * Migrator#shouldMigrate} returns {@code true}.
335          *
336          * <p>The target schema type of the output {@link GenericDocument} of {@link
337          * Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
338          * SetSchemaRequest}.
339          *
340          * @param migrators A {@link Map} of migrators that translate a document from it's current
341          *     version to the final version set via {@link #setVersion}. The key of the map is the
342          *     schema type that the {@link Migrator} value applies to.
343          * @see SetSchemaRequest.Builder#setVersion
344          * @see SetSchemaRequest.Builder#addSchemas
345          * @see AppSearchSession#setSchema
346          */
347         @NonNull
setMigrators(@onNull Map<String, Migrator> migrators)348         public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
349             Objects.requireNonNull(migrators);
350             resetIfBuilt();
351             mMigrators.putAll(migrators);
352             return this;
353         }
354 
355         /**
356          * Sets whether or not to override the current schema in the {@link AppSearchSession}
357          * database.
358          *
359          * <p>Call this method whenever backward incompatible changes need to be made by setting
360          * {@code forceOverride} to {@code true}. As a result, during execution of the setSchema
361          * operation, all documents that are incompatible with the new schema will be deleted and
362          * the new schema will be saved and persisted.
363          *
364          * <p>By default, this is {@code false}.
365          */
366         @NonNull
setForceOverride(boolean forceOverride)367         public Builder setForceOverride(boolean forceOverride) {
368             resetIfBuilt();
369             mForceOverride = forceOverride;
370             return this;
371         }
372 
373         /**
374          * Sets the version number of the overall {@link AppSearchSchema} in the database.
375          *
376          * <p>The {@link AppSearchSession} database can only ever hold documents for one version at
377          * a time.
378          *
379          * <p>Setting a version number that is different from the version number currently stored in
380          * AppSearch will result in AppSearch calling the {@link Migrator}s provided to {@link
381          * AppSearchSession#setSchema} to migrate the documents already in AppSearch from the
382          * previous version to the one set in this request. The version number can be updated
383          * without any other changes to the set of schemas.
384          *
385          * <p>The version number can stay the same, increase, or decrease relative to the current
386          * version number that is already stored in the {@link AppSearchSession} database.
387          *
388          * <p>The version of an empty database will always be 0. You cannot set version to the
389          * {@link SetSchemaRequest}, if it doesn't contains any {@link AppSearchSchema}.
390          *
391          * @param version A positive integer representing the version of the entire set of schemas
392          *     represents the version of the whole schema in the {@link AppSearchSession} database,
393          *     default version is 1.
394          * @throws IllegalArgumentException if the version is negative.
395          * @see AppSearchSession#setSchema
396          * @see Migrator
397          * @see SetSchemaRequest.Builder#setMigrator
398          */
399         @NonNull
setVersion(@ntRangefrom = 1) int version)400         public Builder setVersion(@IntRange(from = 1) int version) {
401             Preconditions.checkArgument(version >= 1, "Version must be a positive number.");
402             resetIfBuilt();
403             mVersion = version;
404             return this;
405         }
406 
407         /**
408          * Builds a new {@link SetSchemaRequest} object.
409          *
410          * @throws IllegalArgumentException if schema types were referenced, but the corresponding
411          *     {@link AppSearchSchema} type was never added.
412          */
413         @NonNull
build()414         public SetSchemaRequest build() {
415             // Verify that any schema types with display or visibility settings refer to a real
416             // schema.
417             // Create a copy because we're going to remove from the set for verification purposes.
418             Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
419             referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
420 
421             for (AppSearchSchema schema : mSchemas) {
422                 referencedSchemas.remove(schema.getSchemaType());
423             }
424             if (!referencedSchemas.isEmpty()) {
425                 // We still have schema types that weren't seen in our mSchemas set. This means
426                 // there wasn't a corresponding AppSearchSchema.
427                 throw new IllegalArgumentException(
428                         "Schema types " + referencedSchemas + " referenced, but were not added.");
429             }
430             if (mSchemas.isEmpty() && mVersion != DEFAULT_VERSION) {
431                 throw new IllegalArgumentException(
432                         "Cannot set version to the request if schema is empty.");
433             }
434             mBuilt = true;
435             return new SetSchemaRequest(
436                     mSchemas,
437                     mSchemasNotDisplayedBySystem,
438                     mSchemasVisibleToPackages,
439                     mMigrators,
440                     mForceOverride,
441                     mVersion);
442         }
443 
resetIfBuilt()444         private void resetIfBuilt() {
445             if (mBuilt) {
446                 ArrayMap<String, Set<PackageIdentifier>> schemasVisibleToPackages =
447                         new ArrayMap<>(mSchemasVisibleToPackages.size());
448                 for (Map.Entry<String, Set<PackageIdentifier>> entry :
449                         mSchemasVisibleToPackages.entrySet()) {
450                     schemasVisibleToPackages.put(entry.getKey(), new ArraySet<>(entry.getValue()));
451                 }
452                 mSchemasVisibleToPackages = schemasVisibleToPackages;
453 
454                 mSchemas = new ArraySet<>(mSchemas);
455                 mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
456                 mMigrators = new ArrayMap<>(mMigrators);
457                 mBuilt = false;
458             }
459         }
460     }
461 }
462