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.FlaggedApi;
20 import android.annotation.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.SuppressLint;
25 import android.app.appsearch.annotation.CanIgnoreReturnValue;
26 import android.util.ArrayMap;
27 import android.util.ArraySet;
28 
29 import com.android.appsearch.flags.Flags;
30 import com.android.internal.util.Preconditions;
31 
32 import java.lang.annotation.Retention;
33 import java.lang.annotation.RetentionPolicy;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.Map;
38 import java.util.Objects;
39 import java.util.Set;
40 
41 /**
42  * Encapsulates a request to update the schema of an {@link AppSearchSession} database.
43  *
44  * <p>The schema is composed of a collection of {@link AppSearchSchema} objects, each of which
45  * defines a unique type of data.
46  *
47  * <p>The first call to SetSchemaRequest will set the provided schema and store it within the {@link
48  * AppSearchSession} database.
49  *
50  * <p>Subsequent calls will compare the provided schema to the previously saved schema, to determine
51  * how to treat existing documents.
52  *
53  * <p>The following types of schema modifications are always safe and are made without deleting any
54  * existing documents:
55  *
56  * <ul>
57  *   <li>Addition of new {@link AppSearchSchema} types
58  *   <li>Addition of new properties to an existing {@link AppSearchSchema} type
59  *   <li>Changing the cardinality of a property to be less restrictive
60  * </ul>
61  *
62  * <p>The following types of schema changes are not backwards compatible:
63  *
64  * <ul>
65  *   <li>Removal of an existing {@link AppSearchSchema} type
66  *   <li>Removal of a property from an existing {@link AppSearchSchema} type
67  *   <li>Changing the data type of an existing property
68  *   <li>Changing the cardinality of a property to be more restrictive
69  * </ul>
70  *
71  * <p>Providing a schema with incompatible changes, will throw an {@link
72  * android.app.appsearch.exceptions.AppSearchException}, with a message describing the
73  * incompatibility. As a result, the previously set schema will remain unchanged.
74  *
75  * <p>Backward incompatible changes can be made by :
76  *
77  * <ul>
78  *   <li>setting {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}. This
79  *       deletes all documents that are incompatible with the new schema. The new schema is then
80  *       saved and persisted to disk.
81  *   <li>Add a {@link Migrator} for each incompatible type and make no deletion. The migrator will
82  *       migrate documents from its old schema version to the new version. Migrated types will be
83  *       set into both {@link SetSchemaResponse#getIncompatibleTypes()} and {@link
84  *       SetSchemaResponse#getMigratedTypes()}. See the migration section below.
85  * </ul>
86  *
87  * @see AppSearchSession#setSchema
88  * @see Migrator
89  */
90 public final class SetSchemaRequest {
91 
92     /**
93      * List of Android Permission are supported in {@link
94      * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
95      *
96      * @see android.Manifest.permission
97      * @hide
98      */
99     @IntDef(
100             value = {
101                 READ_SMS,
102                 READ_CALENDAR,
103                 READ_CONTACTS,
104                 READ_EXTERNAL_STORAGE,
105                 READ_HOME_APP_SEARCH_DATA,
106                 READ_ASSISTANT_APP_SEARCH_DATA,
107                 ENTERPRISE_ACCESS,
108                 MANAGED_PROFILE_CONTACTS_ACCESS,
109             })
110     @Retention(RetentionPolicy.SOURCE)
111     public @interface AppSearchSupportedPermission {}
112 
113     /**
114      * The {@link android.Manifest.permission#READ_SMS} AppSearch supported in {@link
115      * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
116      */
117     public static final int READ_SMS = 1;
118 
119     /**
120      * The {@link android.Manifest.permission#READ_CALENDAR} AppSearch supported in {@link
121      * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
122      */
123     public static final int READ_CALENDAR = 2;
124 
125     /**
126      * The {@link android.Manifest.permission#READ_CONTACTS} AppSearch supported in {@link
127      * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
128      */
129     public static final int READ_CONTACTS = 3;
130 
131     /**
132      * The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} AppSearch supported in {@link
133      * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
134      */
135     public static final int READ_EXTERNAL_STORAGE = 4;
136 
137     /**
138      * The {@link android.Manifest.permission#READ_HOME_APP_SEARCH_DATA} AppSearch supported in
139      * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
140      */
141     public static final int READ_HOME_APP_SEARCH_DATA = 5;
142 
143     /**
144      * The {@link android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA} AppSearch supported in
145      * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
146      */
147     public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6;
148 
149     /**
150      * A schema must have this permission set through {@link
151      * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} to be visible to an
152      * {@link EnterpriseGlobalSearchSession}. A call from a regular {@link GlobalSearchSession} will
153      * not count as having this permission.
154      *
155      * @hide
156      */
157     public static final int ENTERPRISE_ACCESS = 7;
158 
159     /**
160      * A schema with this permission set through {@link
161      * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} requires the caller
162      * to have managed profile contacts access from {@link android.app.admin.DevicePolicyManager} to
163      * be visible. This permission indicates that the protected schema may expose managed profile
164      * data for contacts search.
165      *
166      * @hide
167      */
168     public static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8;
169 
170     private final Set<AppSearchSchema> mSchemas;
171     private final Set<String> mSchemasNotDisplayedBySystem;
172     private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
173     private final Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
174     private final Map<String, PackageIdentifier> mPubliclyVisibleSchemas;
175     private final Map<String, Set<SchemaVisibilityConfig>> mSchemasVisibleToConfigs;
176     private final Map<String, Migrator> mMigrators;
177     private final boolean mForceOverride;
178     private final int mVersion;
179 
SetSchemaRequest( @onNull Set<AppSearchSchema> schemas, @NonNull Set<String> schemasNotDisplayedBySystem, @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages, @NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions, @NonNull Map<String, PackageIdentifier> publiclyVisibleSchemas, @NonNull Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs, @NonNull Map<String, Migrator> migrators, boolean forceOverride, int version)180     SetSchemaRequest(
181             @NonNull Set<AppSearchSchema> schemas,
182             @NonNull Set<String> schemasNotDisplayedBySystem,
183             @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
184             @NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions,
185             @NonNull Map<String, PackageIdentifier> publiclyVisibleSchemas,
186             @NonNull Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs,
187             @NonNull Map<String, Migrator> migrators,
188             boolean forceOverride,
189             int version) {
190         mSchemas = Objects.requireNonNull(schemas);
191         mSchemasNotDisplayedBySystem = Objects.requireNonNull(schemasNotDisplayedBySystem);
192         mSchemasVisibleToPackages = Objects.requireNonNull(schemasVisibleToPackages);
193         mSchemasVisibleToPermissions = Objects.requireNonNull(schemasVisibleToPermissions);
194         mPubliclyVisibleSchemas = Objects.requireNonNull(publiclyVisibleSchemas);
195         mSchemasVisibleToConfigs = Objects.requireNonNull(schemasVisibleToConfigs);
196         mMigrators = Objects.requireNonNull(migrators);
197         mForceOverride = forceOverride;
198         mVersion = version;
199     }
200 
201     /** Returns the {@link AppSearchSchema} types that are part of this request. */
202     @NonNull
getSchemas()203     public Set<AppSearchSchema> getSchemas() {
204         return Collections.unmodifiableSet(mSchemas);
205     }
206 
207     /**
208      * Returns all the schema types that are opted out of being displayed and visible on any system
209      * UI surface.
210      */
211     @NonNull
getSchemasNotDisplayedBySystem()212     public Set<String> getSchemasNotDisplayedBySystem() {
213         return Collections.unmodifiableSet(mSchemasNotDisplayedBySystem);
214     }
215 
216     /**
217      * Returns a mapping of schema types to the set of packages that have access to that schema
218      * type.
219      *
220      * <p>It’s inefficient to call this method repeatedly.
221      */
222     @NonNull
getSchemasVisibleToPackages()223     public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
224         Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
225         for (Map.Entry<String, Set<PackageIdentifier>> entry :
226                 mSchemasVisibleToPackages.entrySet()) {
227             copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
228         }
229         return copy;
230     }
231 
232     /**
233      * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
234      * combinations that querier must hold to access that schema type.
235      *
236      * <p>The querier could read the {@link GenericDocument} objects under the {@code schemaType} if
237      * they holds ALL required permissions of ANY of the individual value sets.
238      *
239      * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
240      * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
241      *
242      * <ul>
243      *   <li>A querier holds both PermissionA and PermissionB has access.
244      *   <li>A querier holds both PermissionC and PermissionD has access.
245      *   <li>A querier holds only PermissionE has access.
246      *   <li>A querier holds both PermissionA and PermissionE has access.
247      *   <li>A querier holds only PermissionA doesn't have access.
248      *   <li>A querier holds both PermissionA and PermissionC doesn't have access.
249      * </ul>
250      *
251      * <p>It’s inefficient to call this method repeatedly.
252      *
253      * @return The map contains schema type and all combinations of required permission for querier
254      *     to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS}, {@link
255      *     SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS}, {@link
256      *     SetSchemaRequest#READ_EXTERNAL_STORAGE}, {@link
257      *     SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and {@link
258      *     SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
259      */
260     // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
261     @NonNull
getRequiredPermissionsForSchemaTypeVisibility()262     public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
263         return deepCopy(mSchemasVisibleToPermissions);
264     }
265 
266     /**
267      * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying the
268      * package the schemas are from.
269      */
270     @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
271     @NonNull
getPubliclyVisibleSchemas()272     public Map<String, PackageIdentifier> getPubliclyVisibleSchemas() {
273         return Collections.unmodifiableMap(mPubliclyVisibleSchemas);
274     }
275 
276     /**
277      * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have
278      * access to that schema type.
279      *
280      * <p>It’s inefficient to call this method repeatedly.
281      *
282      * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig
283      */
284     @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
285     @NonNull
getSchemasVisibleToConfigs()286     public Map<String, Set<SchemaVisibilityConfig>> getSchemasVisibleToConfigs() {
287         Map<String, Set<SchemaVisibilityConfig>> copy = new ArrayMap<>();
288         for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
289                 mSchemasVisibleToConfigs.entrySet()) {
290             copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
291         }
292         return copy;
293     }
294 
295     /**
296      * Returns the map of {@link Migrator}, the key will be the schema type of the {@link Migrator}
297      * associated with.
298      */
299     @NonNull
getMigrators()300     public Map<String, Migrator> getMigrators() {
301         return Collections.unmodifiableMap(mMigrators);
302     }
303 
304     /**
305      * Returns a mapping of {@link AppSearchSchema} types to the set of packages that have access to
306      * that schema type.
307      *
308      * <p>A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
309      * modifiable map. This is not meant to be unhidden and should only be used by internal classes.
310      *
311      * @hide
312      */
313     @NonNull
getSchemasVisibleToPackagesInternal()314     public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackagesInternal() {
315         return mSchemasVisibleToPackages;
316     }
317 
318     /** Returns whether this request will force the schema to be overridden. */
isForceOverride()319     public boolean isForceOverride() {
320         return mForceOverride;
321     }
322 
323     /** Returns the database overall schema version. */
324     @IntRange(from = 1)
getVersion()325     public int getVersion() {
326         return mVersion;
327     }
328 
329     /** Builder for {@link SetSchemaRequest} objects. */
330     public static final class Builder {
331         private static final int DEFAULT_VERSION = 1;
332         private ArraySet<AppSearchSchema> mSchemas = new ArraySet<>();
333         private ArraySet<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
334         private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
335                 new ArrayMap<>();
336         private ArrayMap<String, Set<Set<Integer>>> mSchemasVisibleToPermissions = new ArrayMap<>();
337         private ArrayMap<String, PackageIdentifier> mPubliclyVisibleSchemas = new ArrayMap<>();
338         private ArrayMap<String, Set<SchemaVisibilityConfig>> mSchemaVisibleToConfigs =
339                 new ArrayMap<>();
340         private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
341         private boolean mForceOverride = false;
342         private int mVersion = DEFAULT_VERSION;
343         private boolean mBuilt = false;
344 
345         /**
346          * Adds one or more {@link AppSearchSchema} types to the schema.
347          *
348          * <p>An {@link AppSearchSchema} object represents one type of structured data.
349          *
350          * <p>Any documents of these types will be displayed on system UI surfaces by default.
351          */
352         @CanIgnoreReturnValue
353         @NonNull
addSchemas(@onNull AppSearchSchema... schemas)354         public Builder addSchemas(@NonNull AppSearchSchema... schemas) {
355             Objects.requireNonNull(schemas);
356             resetIfBuilt();
357             return addSchemas(Arrays.asList(schemas));
358         }
359 
360         /**
361          * Adds a collection of {@link AppSearchSchema} objects to the schema.
362          *
363          * <p>An {@link AppSearchSchema} object represents one type of structured data.
364          */
365         @CanIgnoreReturnValue
366         @NonNull
addSchemas(@onNull Collection<AppSearchSchema> schemas)367         public Builder addSchemas(@NonNull Collection<AppSearchSchema> schemas) {
368             Objects.requireNonNull(schemas);
369             resetIfBuilt();
370             mSchemas.addAll(schemas);
371             return this;
372         }
373 
374         /**
375          * Sets whether or not documents from the provided {@code schemaType} will be displayed and
376          * visible on any system UI surface.
377          *
378          * <p>This setting applies to the provided {@code schemaType} only, and does not persist
379          * across {@link AppSearchSession#setSchema} calls.
380          *
381          * <p>The default behavior, if this method is not called, is to allow types to be displayed
382          * on system UI surfaces.
383          *
384          * @param schemaType The name of an {@link AppSearchSchema} within the same {@link
385          *     SetSchemaRequest}, which will be configured.
386          * @param displayed Whether documents of this type will be displayed on system UI surfaces.
387          */
388         // Merged list available from getSchemasNotDisplayedBySystem
389         @CanIgnoreReturnValue
390         @SuppressLint("MissingGetterMatchingBuilder")
391         @NonNull
setSchemaTypeDisplayedBySystem( @onNull String schemaType, boolean displayed)392         public Builder setSchemaTypeDisplayedBySystem(
393                 @NonNull String schemaType, boolean displayed) {
394             Objects.requireNonNull(schemaType);
395             resetIfBuilt();
396             if (displayed) {
397                 mSchemasNotDisplayedBySystem.remove(schemaType);
398             } else {
399                 mSchemasNotDisplayedBySystem.add(schemaType);
400             }
401             return this;
402         }
403 
404         /**
405          * Adds a set of required Android {@link android.Manifest.permission} combination to the
406          * given schema type.
407          *
408          * <p>If the querier holds ALL of the required permissions in this combination, they will
409          * have access to read {@link GenericDocument} objects of the given schema type.
410          *
411          * <p>You can call this method to add multiple permission combinations, and the querier will
412          * have access if they holds ANY of the combinations.
413          *
414          * <p>The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR}, {@link
415          * #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE}, {@link #READ_HOME_APP_SEARCH_DATA} and
416          * {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
417          *
418          * <p>The relationship between permissions added in this method and package visibility
419          * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access the
420          * schema if they match ANY requirements. If you want to set "AND" requirements like a
421          * caller must hold required permissions AND it is a specified package, please use {@link
422          * #addSchemaTypeVisibleToConfig}.
423          *
424          * @see android.Manifest.permission#READ_SMS
425          * @see android.Manifest.permission#READ_CALENDAR
426          * @see android.Manifest.permission#READ_CONTACTS
427          * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
428          * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
429          * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
430          * @param schemaType The schema type to set visibility on.
431          * @param permissions A set of required Android permissions the caller need to hold to
432          *     access {@link GenericDocument} objects that under the given schema.
433          * @throws IllegalArgumentException – if input unsupported permission.
434          */
435         // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
436         // Merged list available from getRequiredPermissionsForSchemaTypeVisibility
437         @CanIgnoreReturnValue
438         @SuppressLint("MissingGetterMatchingBuilder")
439         @NonNull
addRequiredPermissionsForSchemaTypeVisibility( @onNull String schemaType, @AppSearchSupportedPermission @NonNull Set<Integer> permissions)440         public Builder addRequiredPermissionsForSchemaTypeVisibility(
441                 @NonNull String schemaType,
442                 @AppSearchSupportedPermission @NonNull Set<Integer> permissions) {
443             Objects.requireNonNull(schemaType);
444             Objects.requireNonNull(permissions);
445             for (int permission : permissions) {
446                 Preconditions.checkArgumentInRange(
447                         permission, READ_SMS, MANAGED_PROFILE_CONTACTS_ACCESS, "permission");
448             }
449             resetIfBuilt();
450             Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
451             if (visibleToPermissions == null) {
452                 visibleToPermissions = new ArraySet<>();
453                 mSchemasVisibleToPermissions.put(schemaType, visibleToPermissions);
454             }
455             visibleToPermissions.add(permissions);
456             return this;
457         }
458 
459         /** Clears all required permissions combinations for the given schema type. */
460         @CanIgnoreReturnValue
461         @NonNull
clearRequiredPermissionsForSchemaTypeVisibility(@onNull String schemaType)462         public Builder clearRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType) {
463             Objects.requireNonNull(schemaType);
464             resetIfBuilt();
465             mSchemasVisibleToPermissions.remove(schemaType);
466             return this;
467         }
468 
469         /**
470          * Sets whether or not documents from the provided {@code schemaType} can be read by the
471          * specified package.
472          *
473          * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
474          * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
475          *
476          * <p>To opt into one-way data sharing with another application, the developer will need to
477          * explicitly grant the other application’s package name and certificate Read access to its
478          * data.
479          *
480          * <p>For two-way data sharing, both applications need to explicitly grant Read access to
481          * one another.
482          *
483          * <p>By default, data sharing between applications is disabled.
484          *
485          * <p>The relationship between permissions added in this method and package visibility
486          * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access the
487          * schema if they match ANY requirements. If you want to set "AND" requirements like a
488          * caller must hold required permissions AND it is a specified package, please use {@link
489          * #addSchemaTypeVisibleToConfig}.
490          *
491          * @param schemaType The schema type to set visibility on.
492          * @param visible Whether the {@code schemaType} will be visible or not.
493          * @param packageIdentifier Represents the package that will be granted visibility.
494          */
495         // Merged list available from getSchemasVisibleToPackages
496         @CanIgnoreReturnValue
497         @SuppressLint("MissingGetterMatchingBuilder")
498         @NonNull
setSchemaTypeVisibilityForPackage( @onNull String schemaType, boolean visible, @NonNull PackageIdentifier packageIdentifier)499         public Builder setSchemaTypeVisibilityForPackage(
500                 @NonNull String schemaType,
501                 boolean visible,
502                 @NonNull PackageIdentifier packageIdentifier) {
503             Objects.requireNonNull(schemaType);
504             Objects.requireNonNull(packageIdentifier);
505             resetIfBuilt();
506 
507             Set<PackageIdentifier> packageIdentifiers = mSchemasVisibleToPackages.get(schemaType);
508             if (visible) {
509                 if (packageIdentifiers == null) {
510                     packageIdentifiers = new ArraySet<>();
511                 }
512                 packageIdentifiers.add(packageIdentifier);
513                 mSchemasVisibleToPackages.put(schemaType, packageIdentifiers);
514             } else {
515                 if (packageIdentifiers == null) {
516                     // Return early since there was nothing set to begin with.
517                     return this;
518                 }
519                 packageIdentifiers.remove(packageIdentifier);
520                 if (packageIdentifiers.isEmpty()) {
521                     // Remove the entire key so that we don't have empty sets as values.
522                     mSchemasVisibleToPackages.remove(schemaType);
523                 }
524             }
525 
526             return this;
527         }
528 
529         /**
530          * Specify that the schema should be publicly available, to packages which already have
531          * visibility to {@code packageIdentifier}. This visibility is determined by the result of
532          * {@link android.content.pm.PackageManager#canPackageQuery}.
533          *
534          * <p>It is possible for the packageIdentifier parameter to be different from the package
535          * performing the indexing. This might happen in the case of an on-device indexer processing
536          * information about various packages. The visibility will be the same regardless of which
537          * package indexes the document, as the visibility is based on the packageIdentifier
538          * parameter.
539          *
540          * <p>If this is called repeatedly with the same schema, the {@link PackageIdentifier} in
541          * the last call will be used as the "from" package for that schema.
542          *
543          * <p>Calling this with packageIdentifier set to null is valid, and will remove public
544          * visibility for the schema.
545          *
546          * @param schema the schema to make publicly accessible.
547          * @param packageIdentifier if an app can see this package via
548          *     PackageManager#canPackageQuery, it will be able to see the documents of type {@code
549          *     schema}.
550          */
551         // Merged list available from getPubliclyVisibleSchemas
552         @CanIgnoreReturnValue
553         @SuppressLint("MissingGetterMatchingBuilder")
554         @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
555         @NonNull
setPubliclyVisibleSchema( @onNull String schema, @Nullable PackageIdentifier packageIdentifier)556         public Builder setPubliclyVisibleSchema(
557                 @NonNull String schema, @Nullable PackageIdentifier packageIdentifier) {
558             Objects.requireNonNull(schema);
559             resetIfBuilt();
560 
561             // If the package identifier is null or empty we clear public visibility
562             if (packageIdentifier == null || packageIdentifier.getPackageName().isEmpty()) {
563                 mPubliclyVisibleSchemas.remove(schema);
564                 return this;
565             }
566 
567             mPubliclyVisibleSchemas.put(schema, packageIdentifier);
568             return this;
569         }
570 
571         /**
572          * Sets the documents from the provided {@code schemaType} can be read by the caller if they
573          * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.
574          *
575          * <p>The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A caller
576          * must match ALL requirements to access the schema. For example, a caller must hold
577          * required permissions AND it is a specified package.
578          *
579          * <p>You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s,
580          * and the querier will have access if they match ANY of the {@link SchemaVisibilityConfig}.
581          *
582          * @param schemaType The schema type to set visibility on.
583          * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all requirements
584          *     that a call must to match to access the schema.
585          */
586         // Merged list available from getSchemasVisibleToConfigs
587         @CanIgnoreReturnValue
588         @SuppressLint("MissingGetterMatchingBuilder")
589         @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
590         @NonNull
addSchemaTypeVisibleToConfig( @onNull String schemaType, @NonNull SchemaVisibilityConfig schemaVisibilityConfig)591         public Builder addSchemaTypeVisibleToConfig(
592                 @NonNull String schemaType,
593                 @NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
594             Objects.requireNonNull(schemaType);
595             Objects.requireNonNull(schemaVisibilityConfig);
596             resetIfBuilt();
597             Set<SchemaVisibilityConfig> visibleToConfigs = mSchemaVisibleToConfigs.get(schemaType);
598             if (visibleToConfigs == null) {
599                 visibleToConfigs = new ArraySet<>();
600                 mSchemaVisibleToConfigs.put(schemaType, visibleToConfigs);
601             }
602             visibleToConfigs.add(schemaVisibilityConfig);
603             return this;
604         }
605 
606         /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */
607         @CanIgnoreReturnValue
608         @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
609         @NonNull
clearSchemaTypeVisibleToConfigs(@onNull String schemaType)610         public Builder clearSchemaTypeVisibleToConfigs(@NonNull String schemaType) {
611             Objects.requireNonNull(schemaType);
612             resetIfBuilt();
613             mSchemaVisibleToConfigs.remove(schemaType);
614             return this;
615         }
616 
617         /**
618          * Sets the {@link Migrator} associated with the given SchemaType.
619          *
620          * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
621          * from the current version number stored in AppSearch to the final version set via {@link
622          * #setVersion}.
623          *
624          * <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
625          * is different from the final version set via {@link #setVersion} and {@link
626          * Migrator#shouldMigrate} returns {@code true}.
627          *
628          * <p>The target schema type of the output {@link GenericDocument} of {@link
629          * Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
630          * SetSchemaRequest}.
631          *
632          * @param schemaType The schema type to set migrator on.
633          * @param migrator The migrator translates a document from its current version to the final
634          *     version set via {@link #setVersion}.
635          * @see SetSchemaRequest.Builder#setVersion
636          * @see SetSchemaRequest.Builder#addSchemas
637          * @see AppSearchSession#setSchema
638          */
639         @CanIgnoreReturnValue
640         @NonNull
641         @SuppressLint("MissingGetterMatchingBuilder") // Getter return plural objects.
setMigrator(@onNull String schemaType, @NonNull Migrator migrator)642         public Builder setMigrator(@NonNull String schemaType, @NonNull Migrator migrator) {
643             Objects.requireNonNull(schemaType);
644             Objects.requireNonNull(migrator);
645             resetIfBuilt();
646             mMigrators.put(schemaType, migrator);
647             return this;
648         }
649 
650         /**
651          * Sets a Map of {@link Migrator}s.
652          *
653          * <p>The key of the map is the schema type that the {@link Migrator} value applies to.
654          *
655          * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
656          * from the current version number stored in AppSearch to the final version set via {@link
657          * #setVersion}.
658          *
659          * <p>A {@link Migrator} will be invoked if the current version number stored in AppSearch
660          * is different from the final version set via {@link #setVersion} and {@link
661          * Migrator#shouldMigrate} returns {@code true}.
662          *
663          * <p>The target schema type of the output {@link GenericDocument} of {@link
664          * Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this {@link
665          * SetSchemaRequest}.
666          *
667          * @param migrators A {@link Map} of migrators that translate a document from its current
668          *     version to the final version set via {@link #setVersion}. The key of the map is the
669          *     schema type that the {@link Migrator} value applies to.
670          * @see SetSchemaRequest.Builder#setVersion
671          * @see SetSchemaRequest.Builder#addSchemas
672          * @see AppSearchSession#setSchema
673          */
674         @CanIgnoreReturnValue
675         @NonNull
setMigrators(@onNull Map<String, Migrator> migrators)676         public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
677             Objects.requireNonNull(migrators);
678             resetIfBuilt();
679             mMigrators.putAll(migrators);
680             return this;
681         }
682 
683         /**
684          * Sets whether or not to override the current schema in the {@link AppSearchSession}
685          * database.
686          *
687          * <p>Call this method whenever backward incompatible changes need to be made by setting
688          * {@code forceOverride} to {@code true}. As a result, during execution of the setSchema
689          * operation, all documents that are incompatible with the new schema will be deleted and
690          * the new schema will be saved and persisted.
691          *
692          * <p>By default, this is {@code false}.
693          */
694         @CanIgnoreReturnValue
695         @NonNull
setForceOverride(boolean forceOverride)696         public Builder setForceOverride(boolean forceOverride) {
697             resetIfBuilt();
698             mForceOverride = forceOverride;
699             return this;
700         }
701 
702         /**
703          * Sets the version number of the overall {@link AppSearchSchema} in the database.
704          *
705          * <p>The {@link AppSearchSession} database can only ever hold documents for one version at
706          * a time.
707          *
708          * <p>Setting a version number that is different from the version number currently stored in
709          * AppSearch will result in AppSearch calling the {@link Migrator}s provided to {@link
710          * AppSearchSession#setSchema} to migrate the documents already in AppSearch from the
711          * previous version to the one set in this request. The version number can be updated
712          * without any other changes to the set of schemas.
713          *
714          * <p>The version number can stay the same, increase, or decrease relative to the current
715          * version number that is already stored in the {@link AppSearchSession} database.
716          *
717          * <p>The version of an empty database will always be 0. You cannot set version to the
718          * {@link SetSchemaRequest}, if it doesn't contains any {@link AppSearchSchema}.
719          *
720          * @param version A positive integer representing the version of the entire set of schemas
721          *     represents the version of the whole schema in the {@link AppSearchSession} database,
722          *     default version is 1.
723          * @throws IllegalArgumentException if the version is negative.
724          * @see AppSearchSession#setSchema
725          * @see Migrator
726          * @see SetSchemaRequest.Builder#setMigrator
727          */
728         @CanIgnoreReturnValue
729         @NonNull
setVersion(@ntRangefrom = 1) int version)730         public Builder setVersion(@IntRange(from = 1) int version) {
731             Preconditions.checkArgument(version >= 1, "Version must be a positive number.");
732             resetIfBuilt();
733             mVersion = version;
734             return this;
735         }
736 
737         /**
738          * Builds a new {@link SetSchemaRequest} object.
739          *
740          * @throws IllegalArgumentException if schema types were referenced, but the corresponding
741          *     {@link AppSearchSchema} type was never added.
742          */
743         @NonNull
build()744         public SetSchemaRequest build() {
745             // Verify that any schema types with display or visibility settings refer to a real
746             // schema.
747             // Create a copy because we're going to remove from the set for verification purposes.
748             Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
749             referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
750             referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());
751             referencedSchemas.addAll(mPubliclyVisibleSchemas.keySet());
752             referencedSchemas.addAll(mSchemaVisibleToConfigs.keySet());
753 
754             for (AppSearchSchema schema : mSchemas) {
755                 referencedSchemas.remove(schema.getSchemaType());
756             }
757             if (!referencedSchemas.isEmpty()) {
758                 // We still have schema types that weren't seen in our mSchemas set. This means
759                 // there wasn't a corresponding AppSearchSchema.
760                 throw new IllegalArgumentException(
761                         "Schema types " + referencedSchemas + " referenced, but were not added.");
762             }
763             if (mSchemas.isEmpty() && mVersion != DEFAULT_VERSION) {
764                 throw new IllegalArgumentException(
765                         "Cannot set version to the request if schema is empty.");
766             }
767             mBuilt = true;
768             return new SetSchemaRequest(
769                     mSchemas,
770                     mSchemasNotDisplayedBySystem,
771                     mSchemasVisibleToPackages,
772                     mSchemasVisibleToPermissions,
773                     mPubliclyVisibleSchemas,
774                     mSchemaVisibleToConfigs,
775                     mMigrators,
776                     mForceOverride,
777                     mVersion);
778         }
779 
resetIfBuilt()780         private void resetIfBuilt() {
781             if (mBuilt) {
782                 ArrayMap<String, Set<PackageIdentifier>> schemasVisibleToPackages =
783                         new ArrayMap<>(mSchemasVisibleToPackages.size());
784                 for (Map.Entry<String, Set<PackageIdentifier>> entry :
785                         mSchemasVisibleToPackages.entrySet()) {
786                     schemasVisibleToPackages.put(entry.getKey(), new ArraySet<>(entry.getValue()));
787                 }
788                 mSchemasVisibleToPackages = schemasVisibleToPackages;
789 
790                 mPubliclyVisibleSchemas = new ArrayMap<>(mPubliclyVisibleSchemas);
791 
792                 mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);
793 
794                 ArrayMap<String, Set<SchemaVisibilityConfig>> schemaVisibleToConfigs =
795                         new ArrayMap<>(mSchemaVisibleToConfigs.size());
796                 for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
797                         mSchemaVisibleToConfigs.entrySet()) {
798                     schemaVisibleToConfigs.put(entry.getKey(), new ArraySet<>(entry.getValue()));
799                 }
800                 mSchemaVisibleToConfigs = schemaVisibleToConfigs;
801 
802                 mSchemas = new ArraySet<>(mSchemas);
803                 mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
804                 mMigrators = new ArrayMap<>(mMigrators);
805                 mBuilt = false;
806             }
807         }
808     }
809 
deepCopy( @onNull Map<String, Set<Set<Integer>>> original)810     private static ArrayMap<String, Set<Set<Integer>>> deepCopy(
811             @NonNull Map<String, Set<Set<Integer>>> original) {
812         ArrayMap<String, Set<Set<Integer>>> copy = new ArrayMap<>(original.size());
813         for (Map.Entry<String, Set<Set<Integer>>> entry : original.entrySet()) {
814             Set<Set<Integer>> valueCopy = new ArraySet<>();
815             for (Set<Integer> innerValue : entry.getValue()) {
816                 valueCopy.add(new ArraySet<>(innerValue));
817             }
818             copy.put(entry.getKey(), valueCopy);
819         }
820         return copy;
821     }
822 }
823