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