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 androidx.appsearch.app; 18 19 import android.annotation.SuppressLint; 20 import android.os.Build; 21 import android.os.Parcel; 22 import android.util.Log; 23 24 import androidx.annotation.IntRange; 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 import androidx.annotation.OptIn; 28 import androidx.annotation.RequiresFeature; 29 import androidx.annotation.RestrictTo; 30 import androidx.appsearch.annotation.CanIgnoreReturnValue; 31 import androidx.appsearch.annotation.CurrentTimeMillisLong; 32 import androidx.appsearch.annotation.Document; 33 import androidx.appsearch.annotation.SystemApi; 34 import androidx.appsearch.exceptions.AppSearchException; 35 import androidx.appsearch.flags.FlaggedApi; 36 import androidx.appsearch.flags.Flags; 37 import androidx.appsearch.safeparcel.GenericDocumentParcel; 38 import androidx.appsearch.safeparcel.PropertyParcel; 39 import androidx.appsearch.util.IndentingStringBuilder; 40 import androidx.core.os.ParcelCompat; 41 import androidx.core.util.Preconditions; 42 43 import java.lang.reflect.Array; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 import java.util.Collections; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Objects; 50 import java.util.Set; 51 52 /** 53 * Represents a document unit. 54 * 55 * <p>Documents contain structured data conforming to their {@link AppSearchSchema} type. 56 * Each document is uniquely identified by a namespace and a String ID within that namespace. 57 * 58 * <!--@exportToFramework:ifJetpack()--> 59 * <p>Documents are constructed either by using the {@link GenericDocument.Builder} or providing 60 * an annotated {@link Document} data class. 61 * <!--@exportToFramework:else() 62 * <p>Documents are constructed by using the {@link GenericDocument.Builder}. 63 * --> 64 * 65 * @see AppSearchSession#putAsync 66 * @see AppSearchSession#getByDocumentIdAsync 67 * @see AppSearchSession#search 68 */ 69 // TODO(b/384721898): Switch to JSpecify annotations 70 @SuppressWarnings("JSpecifyNullness") 71 public class GenericDocument { 72 private static final String TAG = "AppSearchGenericDocumen"; 73 74 /** The maximum number of indexed properties a document can have. */ 75 private static final int MAX_INDEXED_PROPERTIES = 16; 76 77 /** 78 * Fixed constant synthetic property for parent types. 79 * 80 * <!--@exportToFramework:hide--> 81 */ 82 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 83 public static final String PARENT_TYPES_SYNTHETIC_PROPERTY = "$$__AppSearch__parentTypes"; 84 85 /** 86 * An immutable empty {@link GenericDocument}. 87 * 88 * <!--@exportToFramework:hide--> 89 */ 90 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 91 public static final GenericDocument EMPTY = new GenericDocument.Builder<>("", "", "").build(); 92 93 /** 94 * The maximum number of indexed properties a document can have. 95 * 96 * <p>Indexed properties are properties which are strings where the 97 * {@link AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other 98 * than {@link AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE}, as well as long 99 * properties where the {@link AppSearchSchema.LongPropertyConfig#getIndexingType} value is 100 * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}. 101 * 102 * <!--@exportToFramework:ifJetpack()--> 103 * 104 * @deprecated This is no longer a static value, but depends on SDK version and what AppSearch 105 * implementation is being used. Use {@link Features#getMaxIndexedProperties} instead. 106 * <!--@exportToFramework:else()--> 107 */ 108 // @exportToFramework:startStrip() 109 @Deprecated 110 // @exportToFramework:endStrip() 111 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) getMaxIndexedProperties()112 public static int getMaxIndexedProperties() { 113 return MAX_INDEXED_PROPERTIES; 114 } 115 116 // @exportToFramework:startStrip() 117 118 /** 119 * Converts an instance of a class annotated with \@{@link Document} into an instance of 120 * {@link GenericDocument}. 121 * 122 * @param document An instance of a class annotated with \@{@link Document}. 123 * @return an instance of {@link GenericDocument} produced by converting {@code document}. 124 * @throws AppSearchException if no generated conversion class exists on the classpath for the 125 * given document class or an unexpected error occurs during 126 * conversion. 127 * @see GenericDocument#toDocumentClass 128 */ fromDocumentClass(@onNull Object document)129 public static @NonNull GenericDocument fromDocumentClass(@NonNull Object document) 130 throws AppSearchException { 131 Preconditions.checkNotNull(document); 132 DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance(); 133 DocumentClassFactory<Object> factory = registry.getOrCreateFactory(document); 134 return factory.toGenericDocument(document); 135 } 136 // @exportToFramework:endStrip() 137 138 /** The class to hold all meta data and properties for this {@link GenericDocument}. */ 139 private final GenericDocumentParcel mDocumentParcel; 140 141 /** 142 * Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}. 143 * 144 * @param documentParcel Packaged {@link GenericDocument} data, such as the result of 145 * {@link #getDocumentParcel()}. 146 * @exportToFramework:hide 147 */ 148 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 149 @SuppressWarnings("deprecation") GenericDocument(@onNull GenericDocumentParcel documentParcel)150 public GenericDocument(@NonNull GenericDocumentParcel documentParcel) { 151 mDocumentParcel = Objects.requireNonNull(documentParcel); 152 } 153 154 /** 155 * Creates a new {@link GenericDocument} from an existing instance. 156 * 157 * <p>This method should be only used by constructor of a subclass. 158 */ GenericDocument(@onNull GenericDocument document)159 protected GenericDocument(@NonNull GenericDocument document) { 160 this(document.mDocumentParcel); 161 } 162 163 /** 164 * Writes the {@link GenericDocument} to the given {@link Parcel}. 165 * 166 * @param dest The {@link Parcel} to write to. 167 * @param flags The flags to use for parceling. 168 * @exportToFramework:hide 169 */ 170 // GenericDocument is an open class that can be extended, whereas parcelable classes must be 171 // final in those methods. Thus, we make this a system api to avoid 3p apps depending on it 172 // and getting confused by the inheritability. 173 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) 174 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC) 175 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) writeToParcel(@onNull Parcel dest, int flags)176 public final void writeToParcel(@NonNull Parcel dest, int flags) { 177 Objects.requireNonNull(dest); 178 dest.writeParcelable(mDocumentParcel, flags); 179 } 180 181 /** 182 * Creates a {@link GenericDocument} from a {@link Parcel}. 183 * 184 * @param parcel The {@link Parcel} to read from. 185 * @exportToFramework:hide 186 */ 187 // GenericDocument is an open class that can be extended, whereas parcelable classes must be 188 // final in those methods. Thus, we make this a system api to avoid 3p apps depending on it 189 // and getting confused by the inheritability. 190 @SuppressWarnings("deprecation") 191 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) 192 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC) 193 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) createFromParcel(@onNull Parcel parcel)194 public static @NonNull GenericDocument createFromParcel(@NonNull Parcel parcel) { 195 Objects.requireNonNull(parcel); 196 GenericDocumentParcel documentParcel; 197 // @exportToFramework:startStrip() 198 if (AppSearchEnvironmentFactory.getEnvironmentInstance().getEnvironment() 199 != AppSearchEnvironment.FRAMEWORK_ENVIRONMENT) { 200 // Non-Framework code should use ParcelCompat. 201 documentParcel = 202 ParcelCompat.readParcelable( 203 parcel, GenericDocumentParcel.class.getClassLoader(), 204 GenericDocumentParcel.class); 205 return new GenericDocument(documentParcel); 206 } 207 // @exportToFramework:endStrip() 208 209 // Code built in Framework cannot depend on Androidx libraries. Therefore, we must call 210 // Parcel#readParcelable directly. 211 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 212 documentParcel = 213 parcel.readParcelable( 214 GenericDocumentParcel.class.getClassLoader(), 215 GenericDocumentParcel.class); 216 } else { 217 // The Parcel#readParcelable(ClassLoader, Class) function has a known issue on Android 218 // T. This was fixed on Android U. When on Android T, call the older version of 219 // Parcel#readParcelable. 220 documentParcel = 221 parcel.readParcelable(GenericDocumentParcel.class.getClassLoader()); 222 } 223 return new GenericDocument(documentParcel); 224 } 225 226 /** 227 * Returns the {@link GenericDocumentParcel} holding the values for this 228 * {@link GenericDocument}. 229 * 230 * @exportToFramework:hide 231 */ 232 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) getDocumentParcel()233 public @NonNull GenericDocumentParcel getDocumentParcel() { 234 return mDocumentParcel; 235 } 236 237 /** Returns the unique identifier of the {@link GenericDocument}. */ getId()238 public @NonNull String getId() { 239 return mDocumentParcel.getId(); 240 } 241 242 /** Returns the namespace of the {@link GenericDocument}. */ getNamespace()243 public @NonNull String getNamespace() { 244 return mDocumentParcel.getNamespace(); 245 } 246 247 /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */ getSchemaType()248 public @NonNull String getSchemaType() { 249 return mDocumentParcel.getSchemaType(); 250 } 251 252 /** 253 * Returns the list of parent types of the {@link GenericDocument}'s type. 254 * 255 * <p>It is guaranteed that child types appear before parent types in the list. 256 * 257 * @deprecated Parent types should no longer be set in {@link GenericDocument}. Use 258 * {@link SearchResult.Builder#getParentTypeMap()} instead. 259 * <!--@exportToFramework:hide--> 260 */ 261 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 262 @Deprecated getParentTypes()263 public @Nullable List<String> getParentTypes() { 264 List<String> result = mDocumentParcel.getParentTypes(); 265 if (result == null) { 266 return null; 267 } 268 return Collections.unmodifiableList(result); 269 } 270 271 /** 272 * Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. 273 * 274 * <p>The value is in the {@link System#currentTimeMillis} time base. 275 */ 276 @CurrentTimeMillisLong getCreationTimestampMillis()277 public long getCreationTimestampMillis() { 278 return mDocumentParcel.getCreationTimestampMillis(); 279 } 280 281 /** 282 * Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. 283 * 284 * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of 285 * {@code creationTimestampMillis + ttlMillis}, measured in the {@link System#currentTimeMillis} 286 * time base, the document will be auto-deleted. 287 * 288 * <p>The default value is 0, which means the document is permanent and won't be auto-deleted 289 * until the app is uninstalled or {@link AppSearchSession#removeAsync} is called. 290 */ getTtlMillis()291 public long getTtlMillis() { 292 return mDocumentParcel.getTtlMillis(); 293 } 294 295 /** 296 * Returns the score of the {@link GenericDocument}. 297 * 298 * <p>The score is a query-independent measure of the document's quality, relative to 299 * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type. 300 * 301 * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}. 302 * Documents with higher scores are considered better than documents with lower scores. 303 * 304 * <p>Any non-negative integer can be used a score. 305 */ getScore()306 public int getScore() { 307 return mDocumentParcel.getScore(); 308 } 309 310 /** Returns the names of all properties defined in this document. */ getPropertyNames()311 public @NonNull Set<String> getPropertyNames() { 312 return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames()); 313 } 314 315 /** 316 * Retrieves the property value with the given path as {@link Object}. 317 * 318 * <p>A path can be a simple property name, such as those returned by {@link #getPropertyNames}. 319 * It may also be a dot-delimited path through the nested document hierarchy, with nested 320 * {@link GenericDocument} properties accessed via {@code '.'} and repeated properties 321 * optionally indexed into via {@code [n]}. 322 * 323 * <p>For example, given the following {@link GenericDocument}: 324 * <pre> 325 * (Message) { 326 * from: "sender@example.com" 327 * to: [{ 328 * name: "Albert Einstein" 329 * email: "einstein@example.com" 330 * }, { 331 * name: "Marie Curie" 332 * email: "curie@example.com" 333 * }] 334 * tags: ["important", "inbox"] 335 * subject: "Hello" 336 * } 337 * </pre> 338 * 339 * <p>Here are some example paths and their results: 340 * <ul> 341 * <li>{@code "from"} returns {@code "sender@example.com"} as a {@link String} array with 342 * one element 343 * <li>{@code "to"} returns the two nested documents containing contact information as a 344 * {@link GenericDocument} array with two elements 345 * <li>{@code "to[1]"} returns the second nested document containing Marie Curie's 346 * contact information as a {@link GenericDocument} array with one element 347 * <li>{@code "to[1].email"} returns {@code "curie@example.com"} 348 * <li>{@code "to[100].email"} returns {@code null} as this particular document does not 349 * have that many elements in its {@code "to"} array. 350 * <li>{@code "to.email"} aggregates emails across all nested documents that have them, 351 * returning {@code ["einstein@example.com", "curie@example.com"]} as a {@link String} 352 * array with two elements. 353 * </ul> 354 * 355 * <p>If you know the expected type of the property you are retrieving, it is recommended to use 356 * one of the typed versions of this method instead, such as {@link #getPropertyString} or 357 * {@link #getPropertyStringArray}. 358 * 359 * <p>If the property was assigned as an empty array using one of the 360 * {@code Builder#setProperty} functions, this method will return an empty array. If no such 361 * property exists at all, this method returns {@code null}. 362 * 363 * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else() 364 * <p>Note: If the property is an empty {@link GenericDocument}[] or {@code byte[][]}, 365 * this method will return a {@code null} value in versions of Android prior to 366 * {@link android.os.Build.VERSION_CODES#TIRAMISU Android T}. Starting in Android T it will 367 * return an empty array if the property has been set as an empty array, matching the 368 * behavior of other property types. 369 * --> 370 * 371 * @param path The path to look for. 372 * @return The entry with the given path as an object or {@code null} if there is no such path. 373 * The returned object will be one of the following types: {@code String[]}, {@code long[]}, 374 * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}. 375 */ getProperty(@onNull String path)376 public @Nullable Object getProperty(@NonNull String path) { 377 Objects.requireNonNull(path); 378 Object rawValue = 379 getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/ 0, 380 mDocumentParcel.getPropertyMap()); 381 382 // Unpack the raw value into the types the user expects, if required. 383 if (rawValue instanceof GenericDocumentParcel) { 384 // getRawPropertyFromRawDocument may return a document as a bare documentParcel 385 // as a performance optimization for lookups. 386 GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue); 387 return new GenericDocument[]{document}; 388 } 389 390 if (rawValue instanceof GenericDocumentParcel[]) { 391 // The underlying parcelable of nested GenericDocuments is packed into 392 // a Parcelable array. 393 // We must unpack it into GenericDocument instances. 394 GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue; 395 GenericDocument[] documents = new GenericDocument[docParcels.length]; 396 for (int i = 0; i < docParcels.length; i++) { 397 if (docParcels[i] == null) { 398 Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path); 399 continue; 400 } 401 documents[i] = new GenericDocument(docParcels[i]); 402 } 403 return documents; 404 } 405 406 // Otherwise the raw property is the same as the final property and needs no transformation. 407 return rawValue; 408 } 409 410 /** 411 * Looks up a property path within the given document bundle. 412 * 413 * <p>The return value may be any of GenericDocument's internal repeated storage types 414 * (String[], long[], double[], boolean[], ArrayList<Bundle>, Parcelable[]). 415 * 416 * <p>Usually, this method takes a path and loops over it to get a property from the bundle. 417 * But in the case where we collect documents across repeated nested documents, we need to 418 * recurse back into this method, and so we also keep track of the index into the path. 419 * 420 * @param path the PropertyPath object representing the path 421 * @param pathIndex the index into the path we start at 422 * @param propertyMap the map containing the path we are looking up 423 * @return the raw property 424 */ 425 @OptIn(markerClass = ExperimentalAppSearchApi.class) 426 @SuppressWarnings("deprecation") getRawPropertyFromRawDocument( @onNull PropertyPath path, int pathIndex, @NonNull Map<String, PropertyParcel> propertyMap)427 private static @Nullable Object getRawPropertyFromRawDocument( 428 @NonNull PropertyPath path, int pathIndex, 429 @NonNull Map<String, PropertyParcel> propertyMap) { 430 Objects.requireNonNull(path); 431 Objects.requireNonNull(propertyMap); 432 for (int i = pathIndex; i < path.size(); i++) { 433 PropertyPath.PathSegment segment = path.get(i); 434 Object currentElementValue = propertyMap.get(segment.getPropertyName()); 435 if (currentElementValue == null) { 436 return null; 437 } 438 439 // If the current PathSegment has an index, we now need to update currentElementValue to 440 // contain the value of the indexed property. For example, for a path segment like 441 // "recipients[0]", currentElementValue now contains the value of "recipients" while we 442 // need the value of "recipients[0]". 443 int index = segment.getPropertyIndex(); 444 if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) { 445 // For properties bundle, now we will only get PropertyParcel as the value. 446 PropertyParcel propertyParcel = (PropertyParcel) currentElementValue; 447 448 // Extract the right array element 449 Object extractedValue = null; 450 if (propertyParcel.getStringValues() != null) { 451 String[] stringValues = propertyParcel.getStringValues(); 452 if (stringValues != null && index < stringValues.length) { 453 extractedValue = Arrays.copyOfRange(stringValues, index, index + 1); 454 } 455 } else if (propertyParcel.getLongValues() != null) { 456 long[] longValues = propertyParcel.getLongValues(); 457 if (longValues != null && index < longValues.length) { 458 extractedValue = Arrays.copyOfRange(longValues, index, index + 1); 459 } 460 } else if (propertyParcel.getDoubleValues() != null) { 461 double[] doubleValues = propertyParcel.getDoubleValues(); 462 if (doubleValues != null && index < doubleValues.length) { 463 extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1); 464 } 465 } else if (propertyParcel.getBooleanValues() != null) { 466 boolean[] booleanValues = propertyParcel.getBooleanValues(); 467 if (booleanValues != null && index < booleanValues.length) { 468 extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1); 469 } 470 } else if (propertyParcel.getBytesValues() != null) { 471 byte[][] bytesValues = propertyParcel.getBytesValues(); 472 if (bytesValues != null && index < bytesValues.length) { 473 extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1); 474 } 475 } else if (propertyParcel.getDocumentValues() != null) { 476 // Special optimization: to avoid creating new singleton arrays for traversing 477 // paths we return the bare document parcel in this particular case. 478 GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues(); 479 if (docValues != null && index < docValues.length) { 480 extractedValue = docValues[index]; 481 } 482 } else if (propertyParcel.getEmbeddingValues() != null) { 483 EmbeddingVector[] embeddingValues = propertyParcel.getEmbeddingValues(); 484 if (embeddingValues != null && index < embeddingValues.length) { 485 extractedValue = Arrays.copyOfRange(embeddingValues, index, index + 1); 486 } 487 } else if (propertyParcel.getBlobHandleValues() != null) { 488 AppSearchBlobHandle[] blobHandlesValues = propertyParcel.getBlobHandleValues(); 489 if (blobHandlesValues != null && index < blobHandlesValues.length) { 490 extractedValue = Arrays.copyOfRange(blobHandlesValues, index, index + 1); 491 } 492 } else { 493 throw new IllegalStateException( 494 "Unsupported value type: " + currentElementValue); 495 } 496 currentElementValue = extractedValue; 497 } 498 499 // at the end of the path, either something like "...foo" or "...foo[1]" 500 if (currentElementValue == null || i == path.size() - 1) { 501 if (currentElementValue != null && currentElementValue instanceof PropertyParcel) { 502 // Unlike previous bundle-based implementation, now each 503 // value is wrapped in PropertyParcel. 504 // Here we need to get and return the actual value for non-repeated fields. 505 currentElementValue = ((PropertyParcel) currentElementValue).getValues(); 506 } 507 return currentElementValue; 508 } 509 510 // currentElementValue is now a GenericDocumentParcel or PropertyParcel, 511 // we can continue down the path. 512 if (currentElementValue instanceof GenericDocumentParcel) { 513 propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap(); 514 } else if (currentElementValue instanceof PropertyParcel 515 && ((PropertyParcel) currentElementValue).getDocumentValues() != null) { 516 GenericDocumentParcel[] docParcels = 517 ((PropertyParcel) currentElementValue).getDocumentValues(); 518 if (docParcels != null && docParcels.length == 1) { 519 propertyMap = docParcels[0].getPropertyMap(); 520 continue; 521 } 522 523 // Slowest path: we're collecting values across repeated nested docs. (Example: 524 // given a path like recipient.name, where recipient is a repeated field, we return 525 // a string array where each recipient's name is an array element). 526 // 527 // Performance note: Suppose that we have a property path "a.b.c" where the "a" 528 // property has N document values and each containing a "b" property with M document 529 // values and each of those containing a "c" property with an int array. 530 // 531 // We'll allocate a new ArrayList for each of the "b" properties, add the M int 532 // arrays from the "c" properties to it and then we'll allocate an int array in 533 // flattenAccumulator before returning that (1 + M allocation per "b" property). 534 // 535 // When we're on the "a" properties, we'll allocate an ArrayList and add the N 536 // flattened int arrays returned from the "b" properties to the list. Then we'll 537 // allocate an int array in flattenAccumulator (1 + N ("b" allocs) allocations per 538 // "a"). // So this implementation could incur 1 + N + NM allocs. 539 // 540 // However, we expect the vast majority of getProperty calls to be either for direct 541 // property names (not paths) or else property paths returned from snippetting, 542 // which always refer to exactly one property value and don't aggregate across 543 // repeated values. The implementation is optimized for these two cases, requiring 544 // no additional allocations. So we've decided that the above performance 545 // characteristics are OK for the less used path. 546 if (docParcels != null) { 547 List<Object> accumulator = new ArrayList<>(docParcels.length); 548 for (GenericDocumentParcel docParcel : docParcels) { 549 // recurse as we need to branch 550 Object value = 551 getRawPropertyFromRawDocument( 552 path, /*pathIndex=*/ i + 1, 553 ((GenericDocumentParcel) docParcel).getPropertyMap()); 554 if (value != null) { 555 accumulator.add(value); 556 } 557 } 558 // Break the path traversing loop 559 return flattenAccumulator(accumulator); 560 } 561 } else { 562 Log.e(TAG, "Failed to apply path to document; no nested value found: " + path); 563 return null; 564 } 565 } 566 // Only way to get here is with an empty path list 567 return null; 568 } 569 570 /** 571 * Combines accumulated repeated properties from multiple documents into a single array. 572 * 573 * @param accumulator List containing objects of the following types: {@code String[]}, 574 * {@code long[]}, {@code double[]}, {@code boolean[]}, {@code byte[][]}, 575 * or {@code GenericDocumentParcelable[]}. 576 * @return The result of concatenating each individual list element into a larger array/list of 577 * the same type. 578 */ flattenAccumulator(@onNull List<Object> accumulator)579 private static @Nullable Object flattenAccumulator(@NonNull List<Object> accumulator) { 580 if (accumulator.isEmpty()) { 581 return null; 582 } 583 Object first = accumulator.get(0); 584 if (first instanceof String[]) { 585 int length = 0; 586 for (int i = 0; i < accumulator.size(); i++) { 587 length += ((String[]) accumulator.get(i)).length; 588 } 589 String[] result = new String[length]; 590 int total = 0; 591 for (int i = 0; i < accumulator.size(); i++) { 592 String[] castValue = (String[]) accumulator.get(i); 593 System.arraycopy(castValue, 0, result, total, castValue.length); 594 total += castValue.length; 595 } 596 return result; 597 } 598 if (first instanceof long[]) { 599 int length = 0; 600 for (int i = 0; i < accumulator.size(); i++) { 601 length += ((long[]) accumulator.get(i)).length; 602 } 603 long[] result = new long[length]; 604 int total = 0; 605 for (int i = 0; i < accumulator.size(); i++) { 606 long[] castValue = (long[]) accumulator.get(i); 607 System.arraycopy(castValue, 0, result, total, castValue.length); 608 total += castValue.length; 609 } 610 return result; 611 } 612 if (first instanceof double[]) { 613 int length = 0; 614 for (int i = 0; i < accumulator.size(); i++) { 615 length += ((double[]) accumulator.get(i)).length; 616 } 617 double[] result = new double[length]; 618 int total = 0; 619 for (int i = 0; i < accumulator.size(); i++) { 620 double[] castValue = (double[]) accumulator.get(i); 621 System.arraycopy(castValue, 0, result, total, castValue.length); 622 total += castValue.length; 623 } 624 return result; 625 } 626 if (first instanceof boolean[]) { 627 int length = 0; 628 for (int i = 0; i < accumulator.size(); i++) { 629 length += ((boolean[]) accumulator.get(i)).length; 630 } 631 boolean[] result = new boolean[length]; 632 int total = 0; 633 for (int i = 0; i < accumulator.size(); i++) { 634 boolean[] castValue = (boolean[]) accumulator.get(i); 635 System.arraycopy(castValue, 0, result, total, castValue.length); 636 total += castValue.length; 637 } 638 return result; 639 } 640 if (first instanceof byte[][]) { 641 int length = 0; 642 for (int i = 0; i < accumulator.size(); i++) { 643 length += ((byte[][]) accumulator.get(i)).length; 644 } 645 byte[][] result = new byte[length][]; 646 int total = 0; 647 for (int i = 0; i < accumulator.size(); i++) { 648 byte[][] castValue = (byte[][]) accumulator.get(i); 649 System.arraycopy(castValue, 0, result, total, castValue.length); 650 total += castValue.length; 651 } 652 return result; 653 } 654 if (first instanceof GenericDocumentParcel[]) { 655 int length = 0; 656 for (int i = 0; i < accumulator.size(); i++) { 657 length += ((GenericDocumentParcel[]) accumulator.get(i)).length; 658 } 659 GenericDocumentParcel[] result = new GenericDocumentParcel[length]; 660 int total = 0; 661 for (int i = 0; i < accumulator.size(); i++) { 662 GenericDocumentParcel[] castValue = (GenericDocumentParcel[]) accumulator.get(i); 663 System.arraycopy(castValue, 0, result, total, castValue.length); 664 total += castValue.length; 665 } 666 return result; 667 } 668 throw new IllegalStateException("Unexpected property type: " + first); 669 } 670 671 /** 672 * Retrieves a {@link String} property by path. 673 * 674 * <p>See {@link #getProperty} for a detailed description of the path syntax. 675 * 676 * @param path The path to look for. 677 * @return The first {@link String} associated with the given path or {@code null} if there is 678 * no such value or the value is of a different type. 679 */ getPropertyString(@onNull String path)680 public @Nullable String getPropertyString(@NonNull String path) { 681 Preconditions.checkNotNull(path); 682 String[] propertyArray = getPropertyStringArray(path); 683 if (propertyArray == null || propertyArray.length == 0) { 684 return null; 685 } 686 warnIfSinglePropertyTooLong("String", path, propertyArray.length); 687 return propertyArray[0]; 688 } 689 690 /** 691 * Retrieves a {@code long} property by path. 692 * 693 * <p>See {@link #getProperty} for a detailed description of the path syntax. 694 * 695 * @param path The path to look for. 696 * @return The first {@code long} associated with the given path or default value {@code 0} if 697 * there is no such value or the value is of a different type. 698 */ getPropertyLong(@onNull String path)699 public long getPropertyLong(@NonNull String path) { 700 Preconditions.checkNotNull(path); 701 long[] propertyArray = getPropertyLongArray(path); 702 if (propertyArray == null || propertyArray.length == 0) { 703 return 0; 704 } 705 warnIfSinglePropertyTooLong("Long", path, propertyArray.length); 706 return propertyArray[0]; 707 } 708 709 /** 710 * Retrieves a {@code double} property by path. 711 * 712 * <p>See {@link #getProperty} for a detailed description of the path syntax. 713 * 714 * @param path The path to look for. 715 * @return The first {@code double} associated with the given path or default value {@code 0.0} 716 * if there is no such value or the value is of a different type. 717 */ getPropertyDouble(@onNull String path)718 public double getPropertyDouble(@NonNull String path) { 719 Preconditions.checkNotNull(path); 720 double[] propertyArray = getPropertyDoubleArray(path); 721 if (propertyArray == null || propertyArray.length == 0) { 722 return 0.0; 723 } 724 warnIfSinglePropertyTooLong("Double", path, propertyArray.length); 725 return propertyArray[0]; 726 } 727 728 /** 729 * Retrieves a {@code boolean} property by path. 730 * 731 * <p>See {@link #getProperty} for a detailed description of the path syntax. 732 * 733 * @param path The path to look for. 734 * @return The first {@code boolean} associated with the given path or default value 735 * {@code false} if there is no such value or the value is of a different type. 736 */ getPropertyBoolean(@onNull String path)737 public boolean getPropertyBoolean(@NonNull String path) { 738 Preconditions.checkNotNull(path); 739 boolean[] propertyArray = getPropertyBooleanArray(path); 740 if (propertyArray == null || propertyArray.length == 0) { 741 return false; 742 } 743 warnIfSinglePropertyTooLong("Boolean", path, propertyArray.length); 744 return propertyArray[0]; 745 } 746 747 /** 748 * Retrieves a {@code byte[]} property by path. 749 * 750 * <p>See {@link #getProperty} for a detailed description of the path syntax. 751 * 752 * @param path The path to look for. 753 * @return The first {@code byte[]} associated with the given path or {@code null} if there is 754 * no such value or the value is of a different type. 755 */ getPropertyBytes(@onNull String path)756 public @Nullable byte[] getPropertyBytes(@NonNull String path) { 757 Preconditions.checkNotNull(path); 758 byte[][] propertyArray = getPropertyBytesArray(path); 759 if (propertyArray == null || propertyArray.length == 0) { 760 return null; 761 } 762 warnIfSinglePropertyTooLong("ByteArray", path, propertyArray.length); 763 return propertyArray[0]; 764 } 765 766 /** 767 * Retrieves a {@link GenericDocument} property by path. 768 * 769 * <p>See {@link #getProperty} for a detailed description of the path syntax. 770 * 771 * @param path The path to look for. 772 * @return The first {@link GenericDocument} associated with the given path or {@code null} if 773 * there is no such value or the value is of a different type. 774 */ getPropertyDocument(@onNull String path)775 public @Nullable GenericDocument getPropertyDocument(@NonNull String path) { 776 Preconditions.checkNotNull(path); 777 GenericDocument[] propertyArray = getPropertyDocumentArray(path); 778 if (propertyArray == null || propertyArray.length == 0) { 779 return null; 780 } 781 warnIfSinglePropertyTooLong("Document", path, propertyArray.length); 782 return propertyArray[0]; 783 } 784 785 /** 786 * Retrieves an {@code EmbeddingVector} property by path. 787 * 788 * <p>See {@link #getProperty} for a detailed description of the path syntax. 789 * 790 * @param path The path to look for. 791 * @return The first {@code EmbeddingVector[]} associated with the given path or 792 * {@code null} if there is no such value or the value is of a different type. 793 */ 794 @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) getPropertyEmbedding(@onNull String path)795 public @Nullable EmbeddingVector getPropertyEmbedding(@NonNull String path) { 796 Preconditions.checkNotNull(path); 797 EmbeddingVector[] propertyArray = getPropertyEmbeddingArray(path); 798 if (propertyArray == null || propertyArray.length == 0) { 799 return null; 800 } 801 warnIfSinglePropertyTooLong("Embedding", path, propertyArray.length); 802 return propertyArray[0]; 803 } 804 805 /** 806 * Retrieves an {@link AppSearchBlobHandle} property by path. 807 * 808 * <p>See {@link #getProperty} for a detailed description of the path syntax. 809 * 810 * <p>See {@link AppSearchSession#openBlobForReadAsync} for how to use 811 * {@link AppSearchBlobHandle} to retrieve blob data. 812 * 813 * @param path The path to look for. 814 * @return The first {@link AppSearchBlobHandle} associated with the given path or 815 * {@code null} if there is no such value or the value is of a different type. 816 */ 817 @FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE) 818 @ExperimentalAppSearchApi getPropertyBlobHandle(@onNull String path)819 public @Nullable AppSearchBlobHandle getPropertyBlobHandle(@NonNull String path) { 820 Preconditions.checkNotNull(path); 821 AppSearchBlobHandle[] propertyArray = getPropertyBlobHandleArray(path); 822 if (propertyArray == null || propertyArray.length == 0) { 823 return null; 824 } 825 warnIfSinglePropertyTooLong("BlobHandle", path, propertyArray.length); 826 return propertyArray[0]; 827 } 828 829 /** Prints a warning to logcat if the given propertyLength is greater than 1. */ warnIfSinglePropertyTooLong( @onNull String propertyType, @NonNull String path, int propertyLength)830 private static void warnIfSinglePropertyTooLong( 831 @NonNull String propertyType, @NonNull String path, int propertyLength) { 832 if (propertyLength > 1) { 833 Log.w(TAG, "The value for \"" + path + "\" contains " + propertyLength 834 + " elements. Only the first one will be returned from " 835 + "getProperty" + propertyType + "(). Try getProperty" + propertyType 836 + "Array()."); 837 } 838 } 839 840 /** 841 * Retrieves a repeated {@code String} property by path. 842 * 843 * <p>See {@link #getProperty} for a detailed description of the path syntax. 844 * 845 * <p>If the property has not been set via {@link Builder#setPropertyString}, this method 846 * returns {@code null}. 847 * 848 * <p>If it has been set via {@link Builder#setPropertyString} to an empty 849 * {@code String[]}, this method returns an empty {@code String[]}. 850 * 851 * @param path The path to look for. 852 * @return The {@code String[]} associated with the given path, or {@code null} if no value is 853 * set or the value is of a different type. 854 */ getPropertyStringArray(@onNull String path)855 public @Nullable String[] getPropertyStringArray(@NonNull String path) { 856 Preconditions.checkNotNull(path); 857 Object value = getProperty(path); 858 return safeCastProperty(path, value, String[].class); 859 } 860 861 /** 862 * Retrieves a repeated {@code long[]} property by path. 863 * 864 * <p>See {@link #getProperty} for a detailed description of the path syntax. 865 * 866 * <p>If the property has not been set via {@link Builder#setPropertyLong}, this method 867 * returns {@code null}. 868 * 869 * <p>If it has been set via {@link Builder#setPropertyLong} to an empty 870 * {@code long[]}, this method returns an empty {@code long[]}. 871 * 872 * @param path The path to look for. 873 * @return The {@code long[]} associated with the given path, or {@code null} if no value is 874 * set or the value is of a different type. 875 */ getPropertyLongArray(@onNull String path)876 public @Nullable long[] getPropertyLongArray(@NonNull String path) { 877 Preconditions.checkNotNull(path); 878 Object value = getProperty(path); 879 return safeCastProperty(path, value, long[].class); 880 } 881 882 /** 883 * Retrieves a repeated {@code double} property by path. 884 * 885 * <p>See {@link #getProperty} for a detailed description of the path syntax. 886 * 887 * <p>If the property has not been set via {@link Builder#setPropertyDouble}, this method 888 * returns {@code null}. 889 * 890 * <p>If it has been set via {@link Builder#setPropertyDouble} to an empty 891 * {@code double[]}, this method returns an empty {@code double[]}. 892 * 893 * @param path The path to look for. 894 * @return The {@code double[]} associated with the given path, or {@code null} if no value is 895 * set or the value is of a different type. 896 */ getPropertyDoubleArray(@onNull String path)897 public @Nullable double[] getPropertyDoubleArray(@NonNull String path) { 898 Preconditions.checkNotNull(path); 899 Object value = getProperty(path); 900 return safeCastProperty(path, value, double[].class); 901 } 902 903 /** 904 * Retrieves a repeated {@code boolean} property by path. 905 * 906 * <p>See {@link #getProperty} for a detailed description of the path syntax. 907 * 908 * <p>If the property has not been set via {@link Builder#setPropertyBoolean}, this method 909 * returns {@code null}. 910 * 911 * <p>If it has been set via {@link Builder#setPropertyBoolean} to an empty 912 * {@code boolean[]}, this method returns an empty {@code boolean[]}. 913 * 914 * @param path The path to look for. 915 * @return The {@code boolean[]} associated with the given path, or {@code null} if no value 916 * is set or the value is of a different type. 917 */ getPropertyBooleanArray(@onNull String path)918 public @Nullable boolean[] getPropertyBooleanArray(@NonNull String path) { 919 Preconditions.checkNotNull(path); 920 Object value = getProperty(path); 921 return safeCastProperty(path, value, boolean[].class); 922 } 923 924 /** 925 * Retrieves a {@code byte[][]} property by path. 926 * 927 * <p>See {@link #getProperty} for a detailed description of the path syntax. 928 * 929 * <p>If the property has not been set via {@link Builder#setPropertyBytes}, this method 930 * returns {@code null}. 931 * 932 * <!--@exportToFramework:ifJetpack()--> 933 * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]}, 934 * this method returns an empty {@code byte[][]}. 935 * <!--@exportToFramework:else() 936 * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]}, 937 * this method returns an empty {@code byte[][]} starting in 938 * {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier 939 * versions of Android. 940 * --> 941 * 942 * @param path The path to look for. 943 * @return The {@code byte[][]} associated with the given path, or {@code null} if no value is 944 * set or the value is of a different type. 945 */ 946 @SuppressLint("ArrayReturn") getPropertyBytesArray(@onNull String path)947 public @Nullable byte[][] getPropertyBytesArray(@NonNull String path) { 948 Preconditions.checkNotNull(path); 949 Object value = getProperty(path); 950 return safeCastProperty(path, value, byte[][].class); 951 } 952 953 /** 954 * Retrieves a repeated {@link GenericDocument} property by path. 955 * 956 * <p>See {@link #getProperty} for a detailed description of the path syntax. 957 * 958 * <p>If the property has not been set via {@link Builder#setPropertyDocument}, this method 959 * returns {@code null}. 960 * 961 * <!--@exportToFramework:ifJetpack()--> 962 * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty 963 * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}. 964 * <!--@exportToFramework:else() 965 * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty 966 * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting 967 * in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier 968 * versions of Android. 969 * --> 970 * 971 * @param path The path to look for. 972 * @return The {@link GenericDocument}[] associated with the given path, or {@code null} if no 973 * value is set or the value is of a different type. 974 */ 975 @SuppressLint("ArrayReturn") getPropertyDocumentArray(@onNull String path)976 public @Nullable GenericDocument[] getPropertyDocumentArray(@NonNull String path) { 977 Preconditions.checkNotNull(path); 978 Object value = getProperty(path); 979 return safeCastProperty(path, value, GenericDocument[].class); 980 } 981 982 /** 983 * Retrieves a repeated {@code EmbeddingVector[]} property by path. 984 * 985 * <p>See {@link #getProperty} for a detailed description of the path syntax. 986 * 987 * <p>If the property has not been set via {@link Builder#setPropertyEmbedding}, this method 988 * returns {@code null}. 989 * 990 * <p>If it has been set via {@link Builder#setPropertyEmbedding} to an empty 991 * {@code EmbeddingVector[]}, this method returns an empty 992 * {@code EmbeddingVector[]}. 993 * 994 * @param path The path to look for. 995 * @return The {@code EmbeddingVector[]} associated with the given path, or 996 * {@code null} if no value is set or the value is of a different type. 997 */ 998 @SuppressLint({"ArrayReturn", "NullableCollection"}) 999 @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) getPropertyEmbeddingArray(@onNull String path)1000 public @Nullable EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String path) { 1001 Preconditions.checkNotNull(path); 1002 Object value = getProperty(path); 1003 return safeCastProperty(path, value, EmbeddingVector[].class); 1004 } 1005 1006 /** 1007 * Retrieves a repeated {@code AppSearchBlobHandle[]} property by path. 1008 * 1009 * <p>See {@link #getProperty} for a detailed description of the path syntax. 1010 * 1011 * <p>If the property has not been set via {@link Builder#setPropertyBlobHandle}, this method 1012 * returns {@code null}. 1013 * 1014 * <p>If it has been set via {@link Builder#setPropertyBlobHandle} to an empty 1015 * {@code AppSearchBlobHandle[]}, this method returns an empty 1016 * {@code AppSearchBlobHandle[]}. 1017 * 1018 * @param path The path to look for. 1019 * @return The {@code AppSearchBlobHandle[]} associated with the given path, or 1020 * {@code null} if no value is set or the value is of a different type. 1021 */ 1022 @SuppressLint({"ArrayReturn", "NullableCollection"}) 1023 @ExperimentalAppSearchApi 1024 @FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE) getPropertyBlobHandleArray(@onNull String path)1025 public @Nullable AppSearchBlobHandle[] getPropertyBlobHandleArray(@NonNull String path) { 1026 Preconditions.checkNotNull(path); 1027 Object value = getProperty(path); 1028 return safeCastProperty(path, value, AppSearchBlobHandle[].class); 1029 } 1030 1031 /** 1032 * Casts a repeated property to the provided type, logging an error and returning {@code null} 1033 * if the cast fails. 1034 * 1035 * @param path Path to the property within the document. Used for logging. 1036 * @param value Value of the property 1037 * @param tClass Class to cast the value into 1038 */ safeCastProperty( @onNull String path, @Nullable Object value, @NonNull Class<T> tClass)1039 private static <T> @Nullable T safeCastProperty( 1040 @NonNull String path, @Nullable Object value, @NonNull Class<T> tClass) { 1041 if (value == null) { 1042 return null; 1043 } 1044 try { 1045 return tClass.cast(value); 1046 } catch (ClassCastException e) { 1047 Log.w(TAG, "Error casting to requested type for path \"" + path + "\"", e); 1048 return null; 1049 } 1050 } 1051 1052 // @exportToFramework:startStrip() 1053 1054 /** 1055 * Converts this GenericDocument into an instance of the provided document class. 1056 * 1057 * <p>It is the developer's responsibility to ensure the right kind of document class is being 1058 * supplied here, either by structuring the application code to ensure the document type is 1059 * known, or by checking the return value of {@link #getSchemaType}. 1060 * 1061 * <p>Document properties are identified by {@code String} names. Any that are found are 1062 * assigned into fields of the given document class. As such, the most likely outcome of 1063 * supplying the wrong document class would be an empty or partially populated result. 1064 * 1065 * @param documentClass a class annotated with {@link Document} 1066 * @return an instance of the document class after being converted from a 1067 * {@link GenericDocument} 1068 * @throws AppSearchException if no factory for this document class could be found on the 1069 * classpath. 1070 * @see GenericDocument#fromDocumentClass 1071 */ 1072 @OptIn(markerClass = ExperimentalAppSearchApi.class) toDocumentClass(@onNull Class<T> documentClass)1073 public <T> @NonNull T toDocumentClass(@NonNull Class<T> documentClass) 1074 throws AppSearchException { 1075 return toDocumentClass(documentClass, DocumentClassMappingContext.EMPTY); 1076 } 1077 1078 /** 1079 * Converts this GenericDocument into an instance of the provided document class. 1080 * 1081 * <p>It is the developer's responsibility to ensure the right kind of document class is being 1082 * supplied here, either by structuring the application code to ensure the document type is 1083 * known, or by checking the return value of {@link #getSchemaType}. 1084 * 1085 * <p>Document properties are identified by {@code String} names. Any that are found are 1086 * assigned into fields of the given document class. As such, the most likely outcome of 1087 * supplying the wrong document class would be an empty or partially populated result. 1088 * 1089 * <p>If this GenericDocument's type is recorded as a subtype of the provided 1090 * {@code documentClass}, the method will find an AppSearch document class, using the provided 1091 * {@code documentClassMappingContext}, that is the most concrete and assignable to 1092 * {@code documentClass}, and then deserialize to that class instead. This allows for more 1093 * specific and accurate deserialization of GenericDocuments. If 1094 * {@code documentClassMappingContext} has information missing or we are not able to find a 1095 * candidate assignable to {@code documentClass}, the method will deserialize to 1096 * {@code documentClass} directly. 1097 * 1098 * <p>Assignability is determined by the programing language's type system, and which type is 1099 * more concrete is determined by AppSearch's type system specified via 1100 * {@link AppSearchSchema.Builder#addParentType(String)} or the annotation parameter 1101 * {@link Document#parent()}. 1102 * 1103 * <p>For nested document properties, this method will be called recursively, and 1104 * {@code documentClassMappingContext} will be passed down to the recursive 1105 * calls of this method. 1106 * 1107 * <p>For most use cases, it is recommended to utilize 1108 * {@link SearchResult#getDocument(Class, Map)} instead of calling this method directly. This 1109 * avoids the need to manually create a {@link DocumentClassMappingContext}. 1110 * 1111 * @param documentClass a class annotated with {@link Document} 1112 * @param documentClassMappingContext a {@link DocumentClassMappingContext} instance 1113 * @return an instance of the document class after being converted from a 1114 * {@link GenericDocument} 1115 * @throws AppSearchException if no factory for this document class could be found on the 1116 * classpath. 1117 * @see GenericDocument#fromDocumentClass 1118 */ 1119 @ExperimentalAppSearchApi toDocumentClass(@onNull Class<T> documentClass, @NonNull DocumentClassMappingContext documentClassMappingContext)1120 public <T> @NonNull T toDocumentClass(@NonNull Class<T> documentClass, 1121 @NonNull DocumentClassMappingContext documentClassMappingContext) 1122 throws AppSearchException { 1123 Preconditions.checkNotNull(documentClass); 1124 Preconditions.checkNotNull(documentClassMappingContext); 1125 DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance(); 1126 Class<? extends T> targetClass = findTargetClassToDeserialize(documentClass, 1127 documentClassMappingContext.getDocumentClassMap(), 1128 documentClassMappingContext.getParentTypeMap()); 1129 DocumentClassFactory<? extends T> factory = registry.getOrCreateFactory(targetClass); 1130 return factory.fromGenericDocument(this, documentClassMappingContext); 1131 } 1132 1133 /** 1134 * Find a target class that is assignable to {@code documentClass} to deserialize this 1135 * document, based on the provided {@code documentClassMap} and {@code parentTypeMap}. If 1136 * {@code documentClassMap} is empty, return {@code documentClass} directly. 1137 * If {@code parentTypeMap} does not contain the required parent type information, this 1138 * method will try the deprecated {@link #getParentTypes()}. 1139 * 1140 * <p>This method first tries to find a target class corresponding to the document's own type. 1141 * If that fails, it then tries to find a class corresponding to the document's parent type. 1142 * If that still fails, {@code documentClass} itself will be returned. 1143 */ findTargetClassToDeserialize( @onNull Class<T> documentClass, @NonNull Map<String, List<String>> documentClassMap, @NonNull Map<String, List<String>> parentTypeMap)1144 private <T> @NonNull Class<? extends T> findTargetClassToDeserialize( 1145 @NonNull Class<T> documentClass, 1146 @NonNull Map<String, List<String>> documentClassMap, 1147 @NonNull Map<String, List<String>> parentTypeMap) { 1148 if (documentClassMap.isEmpty()) { 1149 return documentClass; 1150 } 1151 1152 // Find the target class by the doc's original type. 1153 Class<? extends T> targetClass = AppSearchDocumentClassMap.getAssignableClassBySchemaName( 1154 documentClassMap, getSchemaType(), documentClass); 1155 if (targetClass != null) { 1156 return targetClass; 1157 } 1158 1159 // Find the target class by parent types. 1160 List<String> parentTypes; 1161 if (parentTypeMap.containsKey(getSchemaType())) { 1162 parentTypes = parentTypeMap.get(getSchemaType()); 1163 } else { 1164 parentTypes = getParentTypes(); 1165 } 1166 if (parentTypes != null) { 1167 for (int i = 0; i < parentTypes.size(); ++i) { 1168 targetClass = AppSearchDocumentClassMap.getAssignableClassBySchemaName( 1169 documentClassMap, parentTypes.get(i), documentClass); 1170 if (targetClass != null) { 1171 return targetClass; 1172 } 1173 } 1174 } 1175 1176 Log.w(TAG, "Cannot find any compatible target class to deserialize. Perhaps the annotation " 1177 + "processor was not run or the generated document class map was proguarded out?\n" 1178 + "Try to deserialize to " + documentClass.getCanonicalName() + " directly."); 1179 return documentClass; 1180 } 1181 // @exportToFramework:endStrip() 1182 1183 @Override equals(@ullable Object other)1184 public boolean equals(@Nullable Object other) { 1185 if (this == other) { 1186 return true; 1187 } 1188 if (!(other instanceof GenericDocument)) { 1189 return false; 1190 } 1191 GenericDocument otherDocument = (GenericDocument) other; 1192 return mDocumentParcel.equals(otherDocument.mDocumentParcel); 1193 } 1194 1195 @Override hashCode()1196 public int hashCode() { 1197 return mDocumentParcel.hashCode(); 1198 } 1199 1200 @Override toString()1201 public @NonNull String toString() { 1202 IndentingStringBuilder stringBuilder = new IndentingStringBuilder(); 1203 appendGenericDocumentString(stringBuilder); 1204 return stringBuilder.toString(); 1205 } 1206 1207 /** 1208 * Appends a debug string for the {@link GenericDocument} instance to the given string builder. 1209 * 1210 * @param builder the builder to append to. 1211 */ appendGenericDocumentString(@onNull IndentingStringBuilder builder)1212 void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) { 1213 Preconditions.checkNotNull(builder); 1214 1215 builder.append("{\n"); 1216 builder.increaseIndentLevel(); 1217 1218 builder.append("namespace: \"").append(getNamespace()).append("\",\n"); 1219 builder.append("id: \"").append(getId()).append("\",\n"); 1220 builder.append("score: ").append(getScore()).append(",\n"); 1221 builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); 1222 List<String> parentTypes = getParentTypes(); 1223 if (parentTypes != null) { 1224 builder.append("parentTypes: ").append(parentTypes).append("\n"); 1225 } 1226 builder 1227 .append("creationTimestampMillis: ") 1228 .append(getCreationTimestampMillis()) 1229 .append(",\n"); 1230 builder.append("timeToLiveMillis: ").append(getTtlMillis()).append(",\n"); 1231 1232 builder.append("properties: {\n"); 1233 1234 String[] sortedProperties = getPropertyNames().toArray(new String[0]); 1235 Arrays.sort(sortedProperties); 1236 1237 for (int i = 0; i < sortedProperties.length; i++) { 1238 Object property = Preconditions.checkNotNull(getProperty(sortedProperties[i])); 1239 builder.increaseIndentLevel(); 1240 appendPropertyString(sortedProperties[i], property, builder); 1241 if (i != sortedProperties.length - 1) { 1242 builder.append(",\n"); 1243 } 1244 builder.decreaseIndentLevel(); 1245 } 1246 1247 builder.append("\n"); 1248 builder.append("}"); 1249 1250 builder.decreaseIndentLevel(); 1251 builder.append("\n"); 1252 builder.append("}"); 1253 } 1254 1255 /** 1256 * Appends a debug string for the given document property to the given string builder. 1257 * 1258 * @param propertyName name of property to create string for. 1259 * @param property property object to create string for. 1260 * @param builder the builder to append to. 1261 */ appendPropertyString(@onNull String propertyName, @NonNull Object property, @NonNull IndentingStringBuilder builder)1262 private void appendPropertyString(@NonNull String propertyName, @NonNull Object property, 1263 @NonNull IndentingStringBuilder builder) { 1264 Preconditions.checkNotNull(propertyName); 1265 Preconditions.checkNotNull(property); 1266 Preconditions.checkNotNull(builder); 1267 1268 builder.append("\"").append(propertyName).append("\": ["); 1269 if (property instanceof GenericDocument[]) { 1270 GenericDocument[] documentValues = (GenericDocument[]) property; 1271 for (int i = 0; i < documentValues.length; ++i) { 1272 builder.append("\n"); 1273 builder.increaseIndentLevel(); 1274 documentValues[i].appendGenericDocumentString(builder); 1275 if (i != documentValues.length - 1) { 1276 builder.append(","); 1277 } 1278 builder.append("\n"); 1279 builder.decreaseIndentLevel(); 1280 } 1281 } else { 1282 int propertyArrLength = Array.getLength(property); 1283 for (int i = 0; i < propertyArrLength; i++) { 1284 Object propertyElement = Array.get(property, i); 1285 if (propertyElement instanceof String) { 1286 builder.append("\"").append((String) propertyElement).append("\""); 1287 } else if (propertyElement instanceof byte[]) { 1288 builder.append(Arrays.toString((byte[]) propertyElement)); 1289 } else if (propertyElement != null) { 1290 builder.append(propertyElement.toString()); 1291 } 1292 if (i != propertyArrLength - 1) { 1293 builder.append(", "); 1294 } 1295 } 1296 } 1297 builder.append("]"); 1298 } 1299 1300 /** 1301 * The builder class for {@link GenericDocument}. 1302 * 1303 * @param <BuilderType> Type of subclass who extends this. 1304 */ 1305 // This builder is specifically designed to be extended by classes deriving from 1306 // GenericDocument. 1307 @SuppressLint("StaticFinalBuilder") 1308 @SuppressWarnings("rawtypes") 1309 public static class Builder<BuilderType extends Builder> { 1310 private final GenericDocumentParcel.Builder mDocumentParcelBuilder; 1311 private final BuilderType mBuilderTypeInstance; 1312 1313 /** 1314 * Creates a new {@link GenericDocument.Builder}. 1315 * 1316 * <p>Document IDs are unique within a namespace. 1317 * 1318 * <p>The number of namespaces per app should be kept small for efficiency reasons. 1319 * 1320 * @param namespace the namespace to set for the {@link GenericDocument}. 1321 * @param id the unique identifier for the {@link GenericDocument} in its namespace. 1322 * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The 1323 * provided {@code schemaType} must be defined using 1324 * {@link AppSearchSession#setSchemaAsync} prior 1325 * to inserting a document of this {@code schemaType} into the 1326 * AppSearch index using 1327 * {@link AppSearchSession#putAsync}. 1328 * Otherwise, the document will be rejected by 1329 * {@link AppSearchSession#putAsync} with result code 1330 * {@link AppSearchResult#RESULT_NOT_FOUND}. 1331 */ 1332 @SuppressWarnings("unchecked") Builder(@onNull String namespace, @NonNull String id, @NonNull String schemaType)1333 public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) { 1334 Preconditions.checkNotNull(namespace); 1335 Preconditions.checkNotNull(id); 1336 Preconditions.checkNotNull(schemaType); 1337 1338 mBuilderTypeInstance = (BuilderType) this; 1339 mDocumentParcelBuilder = new GenericDocumentParcel.Builder(namespace, id, schemaType); 1340 } 1341 1342 /** 1343 * Creates a new {@link GenericDocument.Builder} from the given 1344 * {@link GenericDocumentParcel.Builder}. 1345 * 1346 * <p>The bundle is NOT copied. 1347 */ 1348 @SuppressWarnings("unchecked") Builder(@onNull GenericDocumentParcel.Builder documentParcelBuilder)1349 Builder(@NonNull GenericDocumentParcel.Builder documentParcelBuilder) { 1350 mDocumentParcelBuilder = Objects.requireNonNull(documentParcelBuilder); 1351 mBuilderTypeInstance = (BuilderType) this; 1352 } 1353 1354 /** 1355 * Creates a new {@link GenericDocument.Builder} from the given GenericDocument. 1356 * 1357 * <p>The GenericDocument is deep copied, that is, it changes to a new GenericDocument 1358 * returned by this function and will NOT affect the original GenericDocument. 1359 */ 1360 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR) Builder(@onNull GenericDocument document)1361 public Builder(@NonNull GenericDocument document) { 1362 this(new GenericDocumentParcel.Builder(document.mDocumentParcel)); 1363 } 1364 1365 /** 1366 * Sets the app-defined namespace this document resides in, changing the value provided 1367 * in the constructor. No special values are reserved or understood by the infrastructure. 1368 * 1369 * <p>Document IDs are unique within a namespace. 1370 * 1371 * <p>The number of namespaces per app should be kept small for efficiency reasons. 1372 */ 1373 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1374 @CanIgnoreReturnValue setNamespace(@onNull String namespace)1375 public @NonNull BuilderType setNamespace(@NonNull String namespace) { 1376 Preconditions.checkNotNull(namespace); 1377 mDocumentParcelBuilder.setNamespace(namespace); 1378 return mBuilderTypeInstance; 1379 } 1380 1381 /** 1382 * Sets the ID of this document, changing the value provided in the constructor. No 1383 * special values are reserved or understood by the infrastructure. 1384 * 1385 * <p>Document IDs are unique within the combination of package, database, and namespace. 1386 * 1387 * <p>Setting a document with a duplicate id will overwrite the original document with 1388 * the new document, enforcing uniqueness within the above constraint. 1389 */ 1390 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1391 @CanIgnoreReturnValue setId(@onNull String id)1392 public @NonNull BuilderType setId(@NonNull String id) { 1393 Preconditions.checkNotNull(id); 1394 mDocumentParcelBuilder.setId(id); 1395 return mBuilderTypeInstance; 1396 } 1397 1398 /** 1399 * Sets the schema type of this document, changing the value provided in the constructor. 1400 * 1401 * <p>To successfully index a document, the schema type must match the name of an 1402 * {@link AppSearchSchema} object previously provided to 1403 * {@link AppSearchSession#setSchemaAsync}. 1404 */ 1405 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1406 @CanIgnoreReturnValue setSchemaType(@onNull String schemaType)1407 public @NonNull BuilderType setSchemaType(@NonNull String schemaType) { 1408 Preconditions.checkNotNull(schemaType); 1409 mDocumentParcelBuilder.setSchemaType(schemaType); 1410 return mBuilderTypeInstance; 1411 } 1412 1413 /** 1414 * Sets the list of parent types of the {@link GenericDocument}'s type. 1415 * 1416 * <p>Child types must appear before parent types in the list. 1417 * 1418 * @deprecated Parent types should no longer be set in {@link GenericDocument}. Use 1419 * {@link SearchResult.Builder#setParentTypeMap(Map)} instead. 1420 * <!--@exportToFramework:hide--> 1421 */ 1422 @CanIgnoreReturnValue 1423 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 1424 @Deprecated setParentTypes(@ullable List<String> parentTypes)1425 public @NonNull BuilderType setParentTypes(@Nullable List<String> parentTypes) { 1426 mDocumentParcelBuilder.setParentTypes(parentTypes); 1427 return mBuilderTypeInstance; 1428 } 1429 1430 /** 1431 * Sets the score of the {@link GenericDocument}. 1432 * 1433 * <p>The score is a query-independent measure of the document's quality, relative to 1434 * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type. 1435 * 1436 * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}. 1437 * Documents with higher scores are considered better than documents with lower scores. 1438 * 1439 * <p>Any non-negative integer can be used a score. By default, scores are set to 0. 1440 * 1441 * @param score any non-negative {@code int} representing the document's score. 1442 * @throws IllegalArgumentException if the score is negative. 1443 */ 1444 @CanIgnoreReturnValue setScore( @ntRangefrom = 0, to = Integer.MAX_VALUE) int score)1445 public @NonNull BuilderType setScore( 1446 @IntRange(from = 0, to = Integer.MAX_VALUE) int score) { 1447 if (score < 0) { 1448 throw new IllegalArgumentException("Document score cannot be negative."); 1449 } 1450 mDocumentParcelBuilder.setScore(score); 1451 return mBuilderTypeInstance; 1452 } 1453 1454 /** 1455 * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds. 1456 * 1457 * <p>This should be set using a value obtained from the {@link System#currentTimeMillis} 1458 * time base. 1459 * 1460 * <p>If this method is not called, this will be set to the time the object is built. 1461 * 1462 * @param creationTimestampMillis a creation timestamp in milliseconds. 1463 */ 1464 @CanIgnoreReturnValue setCreationTimestampMillis( @urrentTimeMillisLong long creationTimestampMillis)1465 public @NonNull BuilderType setCreationTimestampMillis( 1466 @CurrentTimeMillisLong long creationTimestampMillis) { 1467 mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis); 1468 return mBuilderTypeInstance; 1469 } 1470 1471 /** 1472 * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. 1473 * 1474 * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of 1475 * {@code creationTimestampMillis + ttlMillis}, measured in the 1476 * {@link System#currentTimeMillis} time base, the document will be auto-deleted. 1477 * 1478 * <p>The default value is 0, which means the document is permanent and won't be 1479 * auto-deleted until the app is uninstalled or {@link AppSearchSession#removeAsync} is 1480 * called. 1481 * 1482 * @param ttlMillis a non-negative duration in milliseconds. 1483 * @throws IllegalArgumentException if ttlMillis is negative. 1484 */ 1485 @CanIgnoreReturnValue setTtlMillis(long ttlMillis)1486 public @NonNull BuilderType setTtlMillis(long ttlMillis) { 1487 if (ttlMillis < 0) { 1488 throw new IllegalArgumentException("Document ttlMillis cannot be negative."); 1489 } 1490 mDocumentParcelBuilder.setTtlMillis(ttlMillis); 1491 return mBuilderTypeInstance; 1492 } 1493 1494 /** 1495 * Sets one or multiple {@code String} values for a property, replacing its previous 1496 * values. 1497 * 1498 * @param name the name associated with the {@code values}. Must match the name 1499 * for this property as given in 1500 * {@link AppSearchSchema.PropertyConfig#getName}. 1501 * @param values the {@code String} values of the property. 1502 * @throws IllegalArgumentException if no values are provided, or if a passed in 1503 * {@code String} is {@code null} or "". 1504 */ 1505 @CanIgnoreReturnValue setPropertyString(@onNull String name, @NonNull String... values)1506 public @NonNull BuilderType setPropertyString(@NonNull String name, 1507 @NonNull String... values) { 1508 Preconditions.checkNotNull(name); 1509 Preconditions.checkNotNull(values); 1510 validatePropertyName(name); 1511 for (int i = 0; i < values.length; i++) { 1512 if (values[i] == null) { 1513 throw new IllegalArgumentException("The String at " + i + " is null."); 1514 } 1515 } 1516 mDocumentParcelBuilder.putInPropertyMap(name, values); 1517 return mBuilderTypeInstance; 1518 } 1519 1520 /** 1521 * Sets one or multiple {@code boolean} values for a property, replacing its previous 1522 * values. 1523 * 1524 * @param name the name associated with the {@code values}. Must match the name 1525 * for this property as given in 1526 * {@link AppSearchSchema.PropertyConfig#getName}. 1527 * @param values the {@code boolean} values of the property. 1528 * @throws IllegalArgumentException if the name is empty or {@code null}. 1529 */ 1530 @CanIgnoreReturnValue setPropertyBoolean(@onNull String name, @NonNull boolean... values)1531 public @NonNull BuilderType setPropertyBoolean(@NonNull String name, 1532 @NonNull boolean... values) { 1533 Preconditions.checkNotNull(name); 1534 Preconditions.checkNotNull(values); 1535 validatePropertyName(name); 1536 mDocumentParcelBuilder.putInPropertyMap(name, values); 1537 return mBuilderTypeInstance; 1538 } 1539 1540 /** 1541 * Sets one or multiple {@code long} values for a property, replacing its previous 1542 * values. 1543 * 1544 * @param name the name associated with the {@code values}. Must match the name 1545 * for this property as given in 1546 * {@link AppSearchSchema.PropertyConfig#getName}. 1547 * @param values the {@code long} values of the property. 1548 * @throws IllegalArgumentException if the name is empty or {@code null}. 1549 */ 1550 @CanIgnoreReturnValue setPropertyLong(@onNull String name, @NonNull long... values)1551 public @NonNull BuilderType setPropertyLong(@NonNull String name, 1552 @NonNull long... values) { 1553 Preconditions.checkNotNull(name); 1554 Preconditions.checkNotNull(values); 1555 validatePropertyName(name); 1556 mDocumentParcelBuilder.putInPropertyMap(name, values); 1557 return mBuilderTypeInstance; 1558 } 1559 1560 /** 1561 * Sets one or multiple {@code double} values for a property, replacing its previous 1562 * values. 1563 * 1564 * @param name the name associated with the {@code values}. Must match the name 1565 * for this property as given in 1566 * {@link AppSearchSchema.PropertyConfig#getName}. 1567 * @param values the {@code double} values of the property. 1568 * @throws IllegalArgumentException if the name is empty or {@code null}. 1569 */ 1570 @CanIgnoreReturnValue setPropertyDouble(@onNull String name, @NonNull double... values)1571 public @NonNull BuilderType setPropertyDouble(@NonNull String name, 1572 @NonNull double... values) { 1573 Preconditions.checkNotNull(name); 1574 Preconditions.checkNotNull(values); 1575 validatePropertyName(name); 1576 mDocumentParcelBuilder.putInPropertyMap(name, values); 1577 return mBuilderTypeInstance; 1578 } 1579 1580 /** 1581 * Sets one or multiple {@code byte[]} for a property, replacing its previous values. 1582 * 1583 * <p> For large byte data and lazy retrieval, see {@link #setPropertyBlobHandle}. 1584 * 1585 * @param name the name associated with the {@code values}. Must match the name 1586 * for this property as given in 1587 * {@link AppSearchSchema.PropertyConfig#getName}. 1588 * @param values the {@code byte[]} of the property. 1589 * @throws IllegalArgumentException if no values are provided, or if a passed in 1590 * {@code byte[]} is {@code null}, or if name is empty. 1591 */ 1592 @CanIgnoreReturnValue setPropertyBytes(@onNull String name, @NonNull byte[]... values)1593 public @NonNull BuilderType setPropertyBytes(@NonNull String name, 1594 @NonNull byte[]... values) { 1595 Preconditions.checkNotNull(name); 1596 Preconditions.checkNotNull(values); 1597 validatePropertyName(name); 1598 for (int i = 0; i < values.length; i++) { 1599 if (values[i] == null) { 1600 throw new IllegalArgumentException("The byte[] at " + i + " is null."); 1601 } 1602 } 1603 mDocumentParcelBuilder.putInPropertyMap(name, values); 1604 return mBuilderTypeInstance; 1605 } 1606 1607 /** 1608 * Sets one or multiple {@link GenericDocument} values for a property, replacing its 1609 * previous values. 1610 * 1611 * @param name the name associated with the {@code values}. Must match the name 1612 * for this property as given in 1613 * {@link AppSearchSchema.PropertyConfig#getName}. 1614 * @param values the {@link GenericDocument} values of the property. 1615 * @throws IllegalArgumentException if no values are provided, or if a passed in 1616 * {@link GenericDocument} is {@code null}, or if name 1617 * is empty. 1618 */ 1619 @CanIgnoreReturnValue setPropertyDocument( @onNull String name, @NonNull GenericDocument... values)1620 public @NonNull BuilderType setPropertyDocument( 1621 @NonNull String name, @NonNull GenericDocument... values) { 1622 Preconditions.checkNotNull(name); 1623 Preconditions.checkNotNull(values); 1624 validatePropertyName(name); 1625 GenericDocumentParcel[] documentParcels = new GenericDocumentParcel[values.length]; 1626 for (int i = 0; i < values.length; i++) { 1627 if (values[i] == null) { 1628 throw new IllegalArgumentException("The document at " + i + " is null."); 1629 } 1630 documentParcels[i] = values[i].getDocumentParcel(); 1631 } 1632 mDocumentParcelBuilder.putInPropertyMap(name, documentParcels); 1633 return mBuilderTypeInstance; 1634 } 1635 1636 /** 1637 * Sets one or multiple {@code EmbeddingVector} values for a property, replacing 1638 * its previous values. 1639 * 1640 * @param name the name associated with the {@code values}. Must match the name 1641 * for this property as given in 1642 * {@link AppSearchSchema.PropertyConfig#getName}. 1643 * @param values the {@code EmbeddingVector} values of the property. 1644 * @throws IllegalArgumentException if the name is empty or {@code null}. 1645 */ 1646 @CanIgnoreReturnValue 1647 @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) 1648 @RequiresFeature( 1649 enforcement = "androidx.appsearch.app.Features#isFeatureSupported", 1650 name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) setPropertyEmbedding(@onNull String name, @NonNull EmbeddingVector... values)1651 public @NonNull BuilderType setPropertyEmbedding(@NonNull String name, 1652 @NonNull EmbeddingVector... values) { 1653 Preconditions.checkNotNull(name); 1654 Preconditions.checkNotNull(values); 1655 validatePropertyName(name); 1656 for (int i = 0; i < values.length; i++) { 1657 if (values[i] == null) { 1658 throw new IllegalArgumentException( 1659 "The EmbeddingVector at " + i + " is null."); 1660 } 1661 } 1662 mDocumentParcelBuilder.putInPropertyMap(name, values); 1663 return mBuilderTypeInstance; 1664 } 1665 1666 /** 1667 * Sets one or multiple {@link AppSearchBlobHandle} values for a property, replacing 1668 * its previous values. 1669 * 1670 * <p>{@link AppSearchBlobHandle} is a pointer to a blob of data. 1671 * 1672 * <p>Store large byte via the {@link android.os.ParcelFileDescriptor} returned from 1673 * {@link AppSearchSession#openBlobForWriteAsync}. Once the blob data is committed via 1674 * {@link AppSearchSession#commitBlobAsync}, the blob is retrievable via 1675 * {@link AppSearchSession#openBlobForReadAsync}. 1676 * 1677 * @param name the name associated with the {@code values}. Must match the name 1678 * for this property as given in 1679 * {@link AppSearchSchema.PropertyConfig#getName}. 1680 * @param values the {@link AppSearchBlobHandle} values of the property. 1681 * @throws IllegalArgumentException if the name is empty or {@code null}. 1682 */ 1683 @CanIgnoreReturnValue 1684 @FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE) 1685 @ExperimentalAppSearchApi 1686 @RequiresFeature( 1687 enforcement = "androidx.appsearch.app.Features#isFeatureSupported", 1688 name = Features.BLOB_STORAGE) setPropertyBlobHandle(@onNull String name, @NonNull AppSearchBlobHandle... values)1689 public @NonNull BuilderType setPropertyBlobHandle(@NonNull String name, 1690 @NonNull AppSearchBlobHandle... values) { 1691 Preconditions.checkNotNull(name); 1692 Preconditions.checkNotNull(values); 1693 validatePropertyName(name); 1694 for (int i = 0; i < values.length; i++) { 1695 if (values[i] == null) { 1696 throw new IllegalArgumentException( 1697 "The BlobHandle at " + i + " is null."); 1698 } 1699 } 1700 mDocumentParcelBuilder.putInPropertyMap(name, values); 1701 return mBuilderTypeInstance; 1702 } 1703 1704 /** 1705 * Clears the value for the property with the given name. 1706 * 1707 * <p>Note that this method does not support property paths. 1708 * 1709 * <p>You should check for the existence of the property in {@link #getPropertyNames} if 1710 * you need to make sure the property being cleared actually exists. 1711 * 1712 * <p>If the string passed is an invalid or nonexistent property, no error message or 1713 * behavior will be observed. 1714 * 1715 * @param name The name of the property to clear. 1716 */ 1717 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1718 @CanIgnoreReturnValue clearProperty(@onNull String name)1719 public @NonNull BuilderType clearProperty(@NonNull String name) { 1720 Preconditions.checkNotNull(name); 1721 mDocumentParcelBuilder.clearProperty(name); 1722 return mBuilderTypeInstance; 1723 } 1724 1725 /** Builds the {@link GenericDocument} object. */ build()1726 public @NonNull GenericDocument build() { 1727 return new GenericDocument(mDocumentParcelBuilder.build()); 1728 } 1729 1730 /** Method to ensure property names are not blank */ validatePropertyName(@onNull String name)1731 private void validatePropertyName(@NonNull String name) { 1732 if (name.isEmpty()) { 1733 throw new IllegalArgumentException("Property name cannot be blank."); 1734 } 1735 } 1736 } 1737 } 1738