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.CurrentTimeMillisLong; 20 import android.annotation.FlaggedApi; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SuppressLint; 25 import android.app.appsearch.annotation.CanIgnoreReturnValue; 26 import android.app.appsearch.safeparcel.GenericDocumentParcel; 27 import android.app.appsearch.safeparcel.PropertyParcel; 28 import android.app.appsearch.util.IndentingStringBuilder; 29 import android.util.Log; 30 31 import com.android.appsearch.flags.Flags; 32 33 import java.lang.reflect.Array; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Collections; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 import java.util.Set; 41 42 /** 43 * Represents a document unit. 44 * 45 * <p>Documents contain structured data conforming to their {@link AppSearchSchema} type. Each 46 * document is uniquely identified by a namespace and a String ID within that namespace. 47 * 48 * <p>Documents are constructed by using the {@link GenericDocument.Builder}. 49 * 50 * @see AppSearchSession#put 51 * @see AppSearchSession#getByDocumentId 52 * @see AppSearchSession#search 53 */ 54 public class GenericDocument { 55 private static final String TAG = "AppSearchGenericDocumen"; 56 57 /** The maximum number of indexed properties a document can have. */ 58 private static final int MAX_INDEXED_PROPERTIES = 16; 59 60 /** 61 * Fixed constant synthetic property for parent types. 62 * 63 * @hide 64 */ 65 public static final String PARENT_TYPES_SYNTHETIC_PROPERTY = "$$__AppSearch__parentTypes"; 66 67 /** 68 * An immutable empty {@link GenericDocument}. 69 * 70 * @hide 71 */ 72 public static final GenericDocument EMPTY = new GenericDocument.Builder<>("", "", "").build(); 73 74 /** 75 * The maximum number of indexed properties a document can have. 76 * 77 * <p>Indexed properties are properties which are strings where the {@link 78 * AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other than {@link 79 * AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE}, as well as long properties where 80 * the {@link AppSearchSchema.LongPropertyConfig#getIndexingType} value is {@link 81 * AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}. 82 */ getMaxIndexedProperties()83 public static int getMaxIndexedProperties() { 84 return MAX_INDEXED_PROPERTIES; 85 } 86 87 /** The class to hold all meta data and properties for this {@link GenericDocument}. */ 88 private final GenericDocumentParcel mDocumentParcel; 89 90 /** 91 * Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}. 92 * 93 * @param documentParcel Packaged {@link GenericDocument} data, such as the result of {@link 94 * #getDocumentParcel()}. 95 * @hide 96 */ 97 @SuppressWarnings("deprecation") GenericDocument(@onNull GenericDocumentParcel documentParcel)98 public GenericDocument(@NonNull GenericDocumentParcel documentParcel) { 99 mDocumentParcel = Objects.requireNonNull(documentParcel); 100 } 101 102 /** 103 * Creates a new {@link GenericDocument} from an existing instance. 104 * 105 * <p>This method should be only used by constructor of a subclass. 106 */ GenericDocument(@onNull GenericDocument document)107 protected GenericDocument(@NonNull GenericDocument document) { 108 this(document.mDocumentParcel); 109 } 110 111 /** 112 * Returns the {@link GenericDocumentParcel} holding the values for this {@link 113 * GenericDocument}. 114 * 115 * @hide 116 */ 117 @NonNull getDocumentParcel()118 public GenericDocumentParcel getDocumentParcel() { 119 return mDocumentParcel; 120 } 121 122 /** Returns the unique identifier of the {@link GenericDocument}. */ 123 @NonNull getId()124 public String getId() { 125 return mDocumentParcel.getId(); 126 } 127 128 /** Returns the namespace of the {@link GenericDocument}. */ 129 @NonNull getNamespace()130 public String getNamespace() { 131 return mDocumentParcel.getNamespace(); 132 } 133 134 /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */ 135 @NonNull getSchemaType()136 public String getSchemaType() { 137 return mDocumentParcel.getSchemaType(); 138 } 139 140 /** 141 * Returns the list of parent types of the {@link GenericDocument}'s type. 142 * 143 * <p>It is guaranteed that child types appear before parent types in the list. 144 * 145 * @hide 146 */ 147 @Nullable getParentTypes()148 public List<String> getParentTypes() { 149 List<String> result = mDocumentParcel.getParentTypes(); 150 if (result == null) { 151 return null; 152 } 153 return Collections.unmodifiableList(result); 154 } 155 156 /** 157 * Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. 158 * 159 * <p>The value is in the {@link System#currentTimeMillis} time base. 160 */ 161 @CurrentTimeMillisLong getCreationTimestampMillis()162 public long getCreationTimestampMillis() { 163 return mDocumentParcel.getCreationTimestampMillis(); 164 } 165 166 /** 167 * Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. 168 * 169 * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of 170 * {@code creationTimestampMillis + ttlMillis}, measured in the {@link System#currentTimeMillis} 171 * time base, the document will be auto-deleted. 172 * 173 * <p>The default value is 0, which means the document is permanent and won't be auto-deleted 174 * until the app is uninstalled or {@link AppSearchSession#remove} is called. 175 */ getTtlMillis()176 public long getTtlMillis() { 177 return mDocumentParcel.getTtlMillis(); 178 } 179 180 /** 181 * Returns the score of the {@link GenericDocument}. 182 * 183 * <p>The score is a query-independent measure of the document's quality, relative to other 184 * {@link GenericDocument} objects of the same {@link AppSearchSchema} type. 185 * 186 * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}. 187 * Documents with higher scores are considered better than documents with lower scores. 188 * 189 * <p>Any non-negative integer can be used a score. 190 */ getScore()191 public int getScore() { 192 return mDocumentParcel.getScore(); 193 } 194 195 /** Returns the names of all properties defined in this document. */ 196 @NonNull getPropertyNames()197 public Set<String> getPropertyNames() { 198 return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames()); 199 } 200 201 /** 202 * Retrieves the property value with the given path as {@link Object}. 203 * 204 * <p>A path can be a simple property name, such as those returned by {@link #getPropertyNames}. 205 * It may also be a dot-delimited path through the nested document hierarchy, with nested {@link 206 * GenericDocument} properties accessed via {@code '.'} and repeated properties optionally 207 * indexed into via {@code [n]}. 208 * 209 * <p>For example, given the following {@link GenericDocument}: 210 * 211 * <pre> 212 * (Message) { 213 * from: "sender@example.com" 214 * to: [{ 215 * name: "Albert Einstein" 216 * email: "einstein@example.com" 217 * }, { 218 * name: "Marie Curie" 219 * email: "curie@example.com" 220 * }] 221 * tags: ["important", "inbox"] 222 * subject: "Hello" 223 * } 224 * </pre> 225 * 226 * <p>Here are some example paths and their results: 227 * 228 * <ul> 229 * <li>{@code "from"} returns {@code "sender@example.com"} as a {@link String} array with one 230 * element 231 * <li>{@code "to"} returns the two nested documents containing contact information as a 232 * {@link GenericDocument} array with two elements 233 * <li>{@code "to[1]"} returns the second nested document containing Marie Curie's contact 234 * information as a {@link GenericDocument} array with one element 235 * <li>{@code "to[1].email"} returns {@code "curie@example.com"} 236 * <li>{@code "to[100].email"} returns {@code null} as this particular document does not have 237 * that many elements in its {@code "to"} array. 238 * <li>{@code "to.email"} aggregates emails across all nested documents that have them, 239 * returning {@code ["einstein@example.com", "curie@example.com"]} as a {@link String} 240 * array with two elements. 241 * </ul> 242 * 243 * <p>If you know the expected type of the property you are retrieving, it is recommended to use 244 * one of the typed versions of this method instead, such as {@link #getPropertyString} or 245 * {@link #getPropertyStringArray}. 246 * 247 * <p>If the property was assigned as an empty array using one of the {@code 248 * Builder#setProperty} functions, this method will return an empty array. If no such property 249 * exists at all, this method returns {@code null}. 250 * 251 * <p>Note: If the property is an empty {@link GenericDocument}[] or {@code byte[][]}, this 252 * method will return a {@code null} value in versions of Android prior to {@link 253 * android.os.Build.VERSION_CODES#TIRAMISU Android T}. Starting in Android T it will return an 254 * empty array if the property has been set as an empty array, matching the behavior of other 255 * property types. 256 * 257 * @param path The path to look for. 258 * @return The entry with the given path as an object or {@code null} if there is no such path. 259 * The returned object will be one of the following types: {@code String[]}, {@code long[]}, 260 * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}. 261 */ 262 @Nullable getProperty(@onNull String path)263 public Object getProperty(@NonNull String path) { 264 Objects.requireNonNull(path); 265 Object rawValue = 266 getRawPropertyFromRawDocument( 267 new PropertyPath(path), 268 /* pathIndex= */ 0, 269 mDocumentParcel.getPropertyMap()); 270 271 // Unpack the raw value into the types the user expects, if required. 272 if (rawValue instanceof GenericDocumentParcel) { 273 // getRawPropertyFromRawDocument may return a document as a bare documentParcel 274 // as a performance optimization for lookups. 275 GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue); 276 return new GenericDocument[] {document}; 277 } 278 279 if (rawValue instanceof GenericDocumentParcel[]) { 280 // The underlying parcelable of nested GenericDocuments is packed into 281 // a Parcelable array. 282 // We must unpack it into GenericDocument instances. 283 GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue; 284 GenericDocument[] documents = new GenericDocument[docParcels.length]; 285 for (int i = 0; i < docParcels.length; i++) { 286 if (docParcels[i] == null) { 287 Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path); 288 continue; 289 } 290 documents[i] = new GenericDocument(docParcels[i]); 291 } 292 return documents; 293 } 294 295 // Otherwise the raw property is the same as the final property and needs no transformation. 296 return rawValue; 297 } 298 299 /** 300 * Looks up a property path within the given document bundle. 301 * 302 * <p>The return value may be any of GenericDocument's internal repeated storage types 303 * (String[], long[], double[], boolean[], ArrayList<Bundle>, Parcelable[]). 304 * 305 * <p>Usually, this method takes a path and loops over it to get a property from the bundle. But 306 * in the case where we collect documents across repeated nested documents, we need to recurse 307 * back into this method, and so we also keep track of the index into the path. 308 * 309 * @param path the PropertyPath object representing the path 310 * @param pathIndex the index into the path we start at 311 * @param propertyMap the map containing the path we are looking up 312 * @return the raw property 313 */ 314 @Nullable 315 @SuppressWarnings("deprecation") getRawPropertyFromRawDocument( @onNull PropertyPath path, int pathIndex, @NonNull Map<String, PropertyParcel> propertyMap)316 private static Object getRawPropertyFromRawDocument( 317 @NonNull PropertyPath path, 318 int pathIndex, 319 @NonNull Map<String, PropertyParcel> propertyMap) { 320 Objects.requireNonNull(path); 321 Objects.requireNonNull(propertyMap); 322 for (int i = pathIndex; i < path.size(); i++) { 323 PropertyPath.PathSegment segment = path.get(i); 324 Object currentElementValue = propertyMap.get(segment.getPropertyName()); 325 if (currentElementValue == null) { 326 return null; 327 } 328 329 // If the current PathSegment has an index, we now need to update currentElementValue to 330 // contain the value of the indexed property. For example, for a path segment like 331 // "recipients[0]", currentElementValue now contains the value of "recipients" while we 332 // need the value of "recipients[0]". 333 int index = segment.getPropertyIndex(); 334 if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) { 335 // For properties bundle, now we will only get PropertyParcel as the value. 336 PropertyParcel propertyParcel = (PropertyParcel) currentElementValue; 337 338 // Extract the right array element 339 Object extractedValue = null; 340 if (propertyParcel.getStringValues() != null) { 341 String[] stringValues = propertyParcel.getStringValues(); 342 if (stringValues != null && index < stringValues.length) { 343 extractedValue = Arrays.copyOfRange(stringValues, index, index + 1); 344 } 345 } else if (propertyParcel.getLongValues() != null) { 346 long[] longValues = propertyParcel.getLongValues(); 347 if (longValues != null && index < longValues.length) { 348 extractedValue = Arrays.copyOfRange(longValues, index, index + 1); 349 } 350 } else if (propertyParcel.getDoubleValues() != null) { 351 double[] doubleValues = propertyParcel.getDoubleValues(); 352 if (doubleValues != null && index < doubleValues.length) { 353 extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1); 354 } 355 } else if (propertyParcel.getBooleanValues() != null) { 356 boolean[] booleanValues = propertyParcel.getBooleanValues(); 357 if (booleanValues != null && index < booleanValues.length) { 358 extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1); 359 } 360 } else if (propertyParcel.getBytesValues() != null) { 361 byte[][] bytesValues = propertyParcel.getBytesValues(); 362 if (bytesValues != null && index < bytesValues.length) { 363 extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1); 364 } 365 } else if (propertyParcel.getDocumentValues() != null) { 366 // Special optimization: to avoid creating new singleton arrays for traversing 367 // paths we return the bare document parcel in this particular case. 368 GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues(); 369 if (docValues != null && index < docValues.length) { 370 extractedValue = docValues[index]; 371 } 372 } else if (propertyParcel.getEmbeddingValues() != null) { 373 EmbeddingVector[] embeddingValues = propertyParcel.getEmbeddingValues(); 374 if (embeddingValues != null && index < embeddingValues.length) { 375 extractedValue = Arrays.copyOfRange(embeddingValues, index, index + 1); 376 } 377 } else { 378 throw new IllegalStateException( 379 "Unsupported value type: " + currentElementValue); 380 } 381 currentElementValue = extractedValue; 382 } 383 384 // at the end of the path, either something like "...foo" or "...foo[1]" 385 if (currentElementValue == null || i == path.size() - 1) { 386 if (currentElementValue != null && currentElementValue instanceof PropertyParcel) { 387 // Unlike previous bundle-based implementation, now each 388 // value is wrapped in PropertyParcel. 389 // Here we need to get and return the actual value for non-repeated fields. 390 currentElementValue = ((PropertyParcel) currentElementValue).getValues(); 391 } 392 return currentElementValue; 393 } 394 395 // currentElementValue is now a GenericDocumentParcel or PropertyParcel, 396 // we can continue down the path. 397 if (currentElementValue instanceof GenericDocumentParcel) { 398 propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap(); 399 } else if (currentElementValue instanceof PropertyParcel 400 && ((PropertyParcel) currentElementValue).getDocumentValues() != null) { 401 GenericDocumentParcel[] docParcels = 402 ((PropertyParcel) currentElementValue).getDocumentValues(); 403 if (docParcels != null && docParcels.length == 1) { 404 propertyMap = docParcels[0].getPropertyMap(); 405 continue; 406 } 407 408 // Slowest path: we're collecting values across repeated nested docs. (Example: 409 // given a path like recipient.name, where recipient is a repeated field, we return 410 // a string array where each recipient's name is an array element). 411 // 412 // Performance note: Suppose that we have a property path "a.b.c" where the "a" 413 // property has N document values and each containing a "b" property with M document 414 // values and each of those containing a "c" property with an int array. 415 // 416 // We'll allocate a new ArrayList for each of the "b" properties, add the M int 417 // arrays from the "c" properties to it and then we'll allocate an int array in 418 // flattenAccumulator before returning that (1 + M allocation per "b" property). 419 // 420 // When we're on the "a" properties, we'll allocate an ArrayList and add the N 421 // flattened int arrays returned from the "b" properties to the list. Then we'll 422 // allocate an int array in flattenAccumulator (1 + N ("b" allocs) allocations per 423 // "a"). // So this implementation could incur 1 + N + NM allocs. 424 // 425 // However, we expect the vast majority of getProperty calls to be either for direct 426 // property names (not paths) or else property paths returned from snippetting, 427 // which always refer to exactly one property value and don't aggregate across 428 // repeated values. The implementation is optimized for these two cases, requiring 429 // no additional allocations. So we've decided that the above performance 430 // characteristics are OK for the less used path. 431 if (docParcels != null) { 432 List<Object> accumulator = new ArrayList<>(docParcels.length); 433 for (GenericDocumentParcel docParcel : docParcels) { 434 // recurse as we need to branch 435 Object value = 436 getRawPropertyFromRawDocument( 437 path, 438 /* pathIndex= */ i + 1, 439 ((GenericDocumentParcel) docParcel).getPropertyMap()); 440 if (value != null) { 441 accumulator.add(value); 442 } 443 } 444 // Break the path traversing loop 445 return flattenAccumulator(accumulator); 446 } 447 } else { 448 Log.e(TAG, "Failed to apply path to document; no nested value found: " + path); 449 return null; 450 } 451 } 452 // Only way to get here is with an empty path list 453 return null; 454 } 455 456 /** 457 * Combines accumulated repeated properties from multiple documents into a single array. 458 * 459 * @param accumulator List containing objects of the following types: {@code String[]}, {@code 460 * long[]}, {@code double[]}, {@code boolean[]}, {@code byte[][]}, or {@code 461 * GenericDocumentParcelable[]}. 462 * @return The result of concatenating each individual list element into a larger array/list of 463 * the same type. 464 */ 465 @Nullable flattenAccumulator(@onNull List<Object> accumulator)466 private static Object flattenAccumulator(@NonNull List<Object> accumulator) { 467 if (accumulator.isEmpty()) { 468 return null; 469 } 470 Object first = accumulator.get(0); 471 if (first instanceof String[]) { 472 int length = 0; 473 for (int i = 0; i < accumulator.size(); i++) { 474 length += ((String[]) accumulator.get(i)).length; 475 } 476 String[] result = new String[length]; 477 int total = 0; 478 for (int i = 0; i < accumulator.size(); i++) { 479 String[] castValue = (String[]) accumulator.get(i); 480 System.arraycopy(castValue, 0, result, total, castValue.length); 481 total += castValue.length; 482 } 483 return result; 484 } 485 if (first instanceof long[]) { 486 int length = 0; 487 for (int i = 0; i < accumulator.size(); i++) { 488 length += ((long[]) accumulator.get(i)).length; 489 } 490 long[] result = new long[length]; 491 int total = 0; 492 for (int i = 0; i < accumulator.size(); i++) { 493 long[] castValue = (long[]) accumulator.get(i); 494 System.arraycopy(castValue, 0, result, total, castValue.length); 495 total += castValue.length; 496 } 497 return result; 498 } 499 if (first instanceof double[]) { 500 int length = 0; 501 for (int i = 0; i < accumulator.size(); i++) { 502 length += ((double[]) accumulator.get(i)).length; 503 } 504 double[] result = new double[length]; 505 int total = 0; 506 for (int i = 0; i < accumulator.size(); i++) { 507 double[] castValue = (double[]) accumulator.get(i); 508 System.arraycopy(castValue, 0, result, total, castValue.length); 509 total += castValue.length; 510 } 511 return result; 512 } 513 if (first instanceof boolean[]) { 514 int length = 0; 515 for (int i = 0; i < accumulator.size(); i++) { 516 length += ((boolean[]) accumulator.get(i)).length; 517 } 518 boolean[] result = new boolean[length]; 519 int total = 0; 520 for (int i = 0; i < accumulator.size(); i++) { 521 boolean[] castValue = (boolean[]) accumulator.get(i); 522 System.arraycopy(castValue, 0, result, total, castValue.length); 523 total += castValue.length; 524 } 525 return result; 526 } 527 if (first instanceof byte[][]) { 528 int length = 0; 529 for (int i = 0; i < accumulator.size(); i++) { 530 length += ((byte[][]) accumulator.get(i)).length; 531 } 532 byte[][] result = new byte[length][]; 533 int total = 0; 534 for (int i = 0; i < accumulator.size(); i++) { 535 byte[][] castValue = (byte[][]) accumulator.get(i); 536 System.arraycopy(castValue, 0, result, total, castValue.length); 537 total += castValue.length; 538 } 539 return result; 540 } 541 if (first instanceof GenericDocumentParcel[]) { 542 int length = 0; 543 for (int i = 0; i < accumulator.size(); i++) { 544 length += ((GenericDocumentParcel[]) accumulator.get(i)).length; 545 } 546 GenericDocumentParcel[] result = new GenericDocumentParcel[length]; 547 int total = 0; 548 for (int i = 0; i < accumulator.size(); i++) { 549 GenericDocumentParcel[] castValue = (GenericDocumentParcel[]) accumulator.get(i); 550 System.arraycopy(castValue, 0, result, total, castValue.length); 551 total += castValue.length; 552 } 553 return result; 554 } 555 throw new IllegalStateException("Unexpected property type: " + first); 556 } 557 558 /** 559 * Retrieves a {@link String} property by path. 560 * 561 * <p>See {@link #getProperty} for a detailed description of the path syntax. 562 * 563 * @param path The path to look for. 564 * @return The first {@link String} associated with the given path or {@code null} if there is 565 * no such value or the value is of a different type. 566 */ 567 @Nullable getPropertyString(@onNull String path)568 public String getPropertyString(@NonNull String path) { 569 Objects.requireNonNull(path); 570 String[] propertyArray = getPropertyStringArray(path); 571 if (propertyArray == null || propertyArray.length == 0) { 572 return null; 573 } 574 warnIfSinglePropertyTooLong("String", path, propertyArray.length); 575 return propertyArray[0]; 576 } 577 578 /** 579 * Retrieves a {@code long} property by path. 580 * 581 * <p>See {@link #getProperty} for a detailed description of the path syntax. 582 * 583 * @param path The path to look for. 584 * @return The first {@code long} associated with the given path or default value {@code 0} if 585 * there is no such value or the value is of a different type. 586 */ getPropertyLong(@onNull String path)587 public long getPropertyLong(@NonNull String path) { 588 Objects.requireNonNull(path); 589 long[] propertyArray = getPropertyLongArray(path); 590 if (propertyArray == null || propertyArray.length == 0) { 591 return 0; 592 } 593 warnIfSinglePropertyTooLong("Long", path, propertyArray.length); 594 return propertyArray[0]; 595 } 596 597 /** 598 * Retrieves a {@code double} property by path. 599 * 600 * <p>See {@link #getProperty} for a detailed description of the path syntax. 601 * 602 * @param path The path to look for. 603 * @return The first {@code double} associated with the given path or default value {@code 0.0} 604 * if there is no such value or the value is of a different type. 605 */ getPropertyDouble(@onNull String path)606 public double getPropertyDouble(@NonNull String path) { 607 Objects.requireNonNull(path); 608 double[] propertyArray = getPropertyDoubleArray(path); 609 if (propertyArray == null || propertyArray.length == 0) { 610 return 0.0; 611 } 612 warnIfSinglePropertyTooLong("Double", path, propertyArray.length); 613 return propertyArray[0]; 614 } 615 616 /** 617 * Retrieves a {@code boolean} property by path. 618 * 619 * <p>See {@link #getProperty} for a detailed description of the path syntax. 620 * 621 * @param path The path to look for. 622 * @return The first {@code boolean} associated with the given path or default value {@code 623 * false} if there is no such value or the value is of a different type. 624 */ getPropertyBoolean(@onNull String path)625 public boolean getPropertyBoolean(@NonNull String path) { 626 Objects.requireNonNull(path); 627 boolean[] propertyArray = getPropertyBooleanArray(path); 628 if (propertyArray == null || propertyArray.length == 0) { 629 return false; 630 } 631 warnIfSinglePropertyTooLong("Boolean", path, propertyArray.length); 632 return propertyArray[0]; 633 } 634 635 /** 636 * Retrieves a {@code byte[]} property by path. 637 * 638 * <p>See {@link #getProperty} for a detailed description of the path syntax. 639 * 640 * @param path The path to look for. 641 * @return The first {@code byte[]} associated with the given path or {@code null} if there is 642 * no such value or the value is of a different type. 643 */ 644 @Nullable getPropertyBytes(@onNull String path)645 public byte[] getPropertyBytes(@NonNull String path) { 646 Objects.requireNonNull(path); 647 byte[][] propertyArray = getPropertyBytesArray(path); 648 if (propertyArray == null || propertyArray.length == 0) { 649 return null; 650 } 651 warnIfSinglePropertyTooLong("ByteArray", path, propertyArray.length); 652 return propertyArray[0]; 653 } 654 655 /** 656 * Retrieves a {@link GenericDocument} property by path. 657 * 658 * <p>See {@link #getProperty} for a detailed description of the path syntax. 659 * 660 * @param path The path to look for. 661 * @return The first {@link GenericDocument} associated with the given path or {@code null} if 662 * there is no such value or the value is of a different type. 663 */ 664 @Nullable getPropertyDocument(@onNull String path)665 public GenericDocument getPropertyDocument(@NonNull String path) { 666 Objects.requireNonNull(path); 667 GenericDocument[] propertyArray = getPropertyDocumentArray(path); 668 if (propertyArray == null || propertyArray.length == 0) { 669 return null; 670 } 671 warnIfSinglePropertyTooLong("Document", path, propertyArray.length); 672 return propertyArray[0]; 673 } 674 675 /** 676 * Retrieves an {@code EmbeddingVector} property by path. 677 * 678 * <p>See {@link #getProperty} for a detailed description of the path syntax. 679 * 680 * @param path The path to look for. 681 * @return The first {@code EmbeddingVector[]} associated with the given path or {@code null} if 682 * there is no such value or the value is of a different type. 683 */ 684 @Nullable 685 @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) getPropertyEmbedding(@onNull String path)686 public EmbeddingVector getPropertyEmbedding(@NonNull String path) { 687 Objects.requireNonNull(path); 688 EmbeddingVector[] propertyArray = getPropertyEmbeddingArray(path); 689 if (propertyArray == null || propertyArray.length == 0) { 690 return null; 691 } 692 warnIfSinglePropertyTooLong("Embedding", path, propertyArray.length); 693 return propertyArray[0]; 694 } 695 696 /** Prints a warning to logcat if the given propertyLength is greater than 1. */ warnIfSinglePropertyTooLong( @onNull String propertyType, @NonNull String path, int propertyLength)697 private static void warnIfSinglePropertyTooLong( 698 @NonNull String propertyType, @NonNull String path, int propertyLength) { 699 if (propertyLength > 1) { 700 Log.w( 701 TAG, 702 "The value for \"" 703 + path 704 + "\" contains " 705 + propertyLength 706 + " elements. Only the first one will be returned from " 707 + "getProperty" 708 + propertyType 709 + "(). Try getProperty" 710 + propertyType 711 + "Array()."); 712 } 713 } 714 715 /** 716 * Retrieves a repeated {@code String} property by path. 717 * 718 * <p>See {@link #getProperty} for a detailed description of the path syntax. 719 * 720 * <p>If the property has not been set via {@link Builder#setPropertyString}, this method 721 * returns {@code null}. 722 * 723 * <p>If it has been set via {@link Builder#setPropertyString} to an empty {@code String[]}, 724 * this method returns an empty {@code String[]}. 725 * 726 * @param path The path to look for. 727 * @return The {@code String[]} associated with the given path, or {@code null} if no value is 728 * set or the value is of a different type. 729 */ 730 @Nullable getPropertyStringArray(@onNull String path)731 public String[] getPropertyStringArray(@NonNull String path) { 732 Objects.requireNonNull(path); 733 Object value = getProperty(path); 734 return safeCastProperty(path, value, String[].class); 735 } 736 737 /** 738 * Retrieves a repeated {@code long[]} property by path. 739 * 740 * <p>See {@link #getProperty} for a detailed description of the path syntax. 741 * 742 * <p>If the property has not been set via {@link Builder#setPropertyLong}, this method returns 743 * {@code null}. 744 * 745 * <p>If it has been set via {@link Builder#setPropertyLong} to an empty {@code long[]}, this 746 * method returns an empty {@code long[]}. 747 * 748 * @param path The path to look for. 749 * @return The {@code long[]} associated with the given path, or {@code null} if no value is set 750 * or the value is of a different type. 751 */ 752 @Nullable getPropertyLongArray(@onNull String path)753 public long[] getPropertyLongArray(@NonNull String path) { 754 Objects.requireNonNull(path); 755 Object value = getProperty(path); 756 return safeCastProperty(path, value, long[].class); 757 } 758 759 /** 760 * Retrieves a repeated {@code double} property by path. 761 * 762 * <p>See {@link #getProperty} for a detailed description of the path syntax. 763 * 764 * <p>If the property has not been set via {@link Builder#setPropertyDouble}, this method 765 * returns {@code null}. 766 * 767 * <p>If it has been set via {@link Builder#setPropertyDouble} to an empty {@code double[]}, 768 * this method returns an empty {@code double[]}. 769 * 770 * @param path The path to look for. 771 * @return The {@code double[]} associated with the given path, or {@code null} if no value is 772 * set or the value is of a different type. 773 */ 774 @Nullable getPropertyDoubleArray(@onNull String path)775 public double[] getPropertyDoubleArray(@NonNull String path) { 776 Objects.requireNonNull(path); 777 Object value = getProperty(path); 778 return safeCastProperty(path, value, double[].class); 779 } 780 781 /** 782 * Retrieves a repeated {@code boolean} property by path. 783 * 784 * <p>See {@link #getProperty} for a detailed description of the path syntax. 785 * 786 * <p>If the property has not been set via {@link Builder#setPropertyBoolean}, this method 787 * returns {@code null}. 788 * 789 * <p>If it has been set via {@link Builder#setPropertyBoolean} to an empty {@code boolean[]}, 790 * this method returns an empty {@code boolean[]}. 791 * 792 * @param path The path to look for. 793 * @return The {@code boolean[]} associated with the given path, or {@code null} if no value is 794 * set or the value is of a different type. 795 */ 796 @Nullable getPropertyBooleanArray(@onNull String path)797 public boolean[] getPropertyBooleanArray(@NonNull String path) { 798 Objects.requireNonNull(path); 799 Object value = getProperty(path); 800 return safeCastProperty(path, value, boolean[].class); 801 } 802 803 /** 804 * Retrieves a {@code byte[][]} property by path. 805 * 806 * <p>See {@link #getProperty} for a detailed description of the path syntax. 807 * 808 * <p>If the property has not been set via {@link Builder#setPropertyBytes}, this method returns 809 * {@code null}. 810 * 811 * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]}, this 812 * method returns an empty {@code byte[][]} starting in {@link 813 * android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier versions of 814 * Android. 815 * 816 * @param path The path to look for. 817 * @return The {@code byte[][]} associated with the given path, or {@code null} if no value is 818 * set or the value is of a different type. 819 */ 820 @SuppressLint("ArrayReturn") 821 @Nullable getPropertyBytesArray(@onNull String path)822 public byte[][] getPropertyBytesArray(@NonNull String path) { 823 Objects.requireNonNull(path); 824 Object value = getProperty(path); 825 return safeCastProperty(path, value, byte[][].class); 826 } 827 828 /** 829 * Retrieves a repeated {@link GenericDocument} property by path. 830 * 831 * <p>See {@link #getProperty} for a detailed description of the path syntax. 832 * 833 * <p>If the property has not been set via {@link Builder#setPropertyDocument}, this method 834 * returns {@code null}. 835 * 836 * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty {@code 837 * GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting in {@link 838 * android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier versions of 839 * Android. 840 * 841 * @param path The path to look for. 842 * @return The {@link GenericDocument}[] associated with the given path, or {@code null} if no 843 * value is set or the value is of a different type. 844 */ 845 @SuppressLint("ArrayReturn") 846 @Nullable getPropertyDocumentArray(@onNull String path)847 public GenericDocument[] getPropertyDocumentArray(@NonNull String path) { 848 Objects.requireNonNull(path); 849 Object value = getProperty(path); 850 return safeCastProperty(path, value, GenericDocument[].class); 851 } 852 853 /** 854 * Retrieves a repeated {@code EmbeddingVector[]} property by path. 855 * 856 * <p>See {@link #getProperty} for a detailed description of the path syntax. 857 * 858 * <p>If the property has not been set via {@link Builder#setPropertyEmbedding}, this method 859 * returns {@code null}. 860 * 861 * <p>If it has been set via {@link Builder#setPropertyEmbedding} to an empty {@code 862 * EmbeddingVector[]}, this method returns an empty {@code EmbeddingVector[]}. 863 * 864 * @param path The path to look for. 865 * @return The {@code EmbeddingVector[]} associated with the given path, or {@code null} if no 866 * value is set or the value is of a different type. 867 */ 868 @SuppressLint({"ArrayReturn", "NullableCollection"}) 869 @Nullable 870 @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) getPropertyEmbeddingArray(@onNull String path)871 public EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String path) { 872 Objects.requireNonNull(path); 873 Object value = getProperty(path); 874 return safeCastProperty(path, value, EmbeddingVector[].class); 875 } 876 877 /** 878 * Casts a repeated property to the provided type, logging an error and returning {@code null} 879 * if the cast fails. 880 * 881 * @param path Path to the property within the document. Used for logging. 882 * @param value Value of the property 883 * @param tClass Class to cast the value into 884 */ 885 @Nullable safeCastProperty( @onNull String path, @Nullable Object value, @NonNull Class<T> tClass)886 private static <T> T safeCastProperty( 887 @NonNull String path, @Nullable Object value, @NonNull Class<T> tClass) { 888 if (value == null) { 889 return null; 890 } 891 try { 892 return tClass.cast(value); 893 } catch (ClassCastException e) { 894 Log.w(TAG, "Error casting to requested type for path \"" + path + "\"", e); 895 return null; 896 } 897 } 898 899 /** 900 * Copies the contents of this {@link GenericDocument} into a new {@link 901 * GenericDocument.Builder}. 902 * 903 * <p>The returned builder is a deep copy whose data is separate from this document. 904 * 905 * @deprecated This API is not compliant with API guidelines. Use {@link 906 * Builder#Builder(GenericDocument)} instead. 907 * @hide 908 */ 909 // TODO(b/171882200): Expose this API in Android T 910 @NonNull 911 @Deprecated toBuilder()912 public GenericDocument.Builder<GenericDocument.Builder<?>> toBuilder() { 913 return new Builder<>(new GenericDocumentParcel.Builder(mDocumentParcel)); 914 } 915 916 @Override equals(@ullable Object other)917 public boolean equals(@Nullable Object other) { 918 if (this == other) { 919 return true; 920 } 921 if (!(other instanceof GenericDocument)) { 922 return false; 923 } 924 GenericDocument otherDocument = (GenericDocument) other; 925 return mDocumentParcel.equals(otherDocument.mDocumentParcel); 926 } 927 928 @Override hashCode()929 public int hashCode() { 930 return mDocumentParcel.hashCode(); 931 } 932 933 @Override 934 @NonNull toString()935 public String toString() { 936 IndentingStringBuilder stringBuilder = new IndentingStringBuilder(); 937 appendGenericDocumentString(stringBuilder); 938 return stringBuilder.toString(); 939 } 940 941 /** 942 * Appends a debug string for the {@link GenericDocument} instance to the given string builder. 943 * 944 * @param builder the builder to append to. 945 */ appendGenericDocumentString(@onNull IndentingStringBuilder builder)946 void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) { 947 Objects.requireNonNull(builder); 948 949 builder.append("{\n"); 950 builder.increaseIndentLevel(); 951 952 builder.append("namespace: \"").append(getNamespace()).append("\",\n"); 953 builder.append("id: \"").append(getId()).append("\",\n"); 954 builder.append("score: ").append(getScore()).append(",\n"); 955 builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); 956 List<String> parentTypes = getParentTypes(); 957 if (parentTypes != null) { 958 builder.append("parentTypes: ").append(parentTypes).append("\n"); 959 } 960 builder.append("creationTimestampMillis: ") 961 .append(getCreationTimestampMillis()) 962 .append(",\n"); 963 builder.append("timeToLiveMillis: ").append(getTtlMillis()).append(",\n"); 964 965 builder.append("properties: {\n"); 966 967 String[] sortedProperties = getPropertyNames().toArray(new String[0]); 968 Arrays.sort(sortedProperties); 969 970 for (int i = 0; i < sortedProperties.length; i++) { 971 Object property = Objects.requireNonNull(getProperty(sortedProperties[i])); 972 builder.increaseIndentLevel(); 973 appendPropertyString(sortedProperties[i], property, builder); 974 if (i != sortedProperties.length - 1) { 975 builder.append(",\n"); 976 } 977 builder.decreaseIndentLevel(); 978 } 979 980 builder.append("\n"); 981 builder.append("}"); 982 983 builder.decreaseIndentLevel(); 984 builder.append("\n"); 985 builder.append("}"); 986 } 987 988 /** 989 * Appends a debug string for the given document property to the given string builder. 990 * 991 * @param propertyName name of property to create string for. 992 * @param property property object to create string for. 993 * @param builder the builder to append to. 994 */ appendPropertyString( @onNull String propertyName, @NonNull Object property, @NonNull IndentingStringBuilder builder)995 private void appendPropertyString( 996 @NonNull String propertyName, 997 @NonNull Object property, 998 @NonNull IndentingStringBuilder builder) { 999 Objects.requireNonNull(propertyName); 1000 Objects.requireNonNull(property); 1001 Objects.requireNonNull(builder); 1002 1003 builder.append("\"").append(propertyName).append("\": ["); 1004 if (property instanceof GenericDocument[]) { 1005 GenericDocument[] documentValues = (GenericDocument[]) property; 1006 for (int i = 0; i < documentValues.length; ++i) { 1007 builder.append("\n"); 1008 builder.increaseIndentLevel(); 1009 documentValues[i].appendGenericDocumentString(builder); 1010 if (i != documentValues.length - 1) { 1011 builder.append(","); 1012 } 1013 builder.append("\n"); 1014 builder.decreaseIndentLevel(); 1015 } 1016 } else { 1017 int propertyArrLength = Array.getLength(property); 1018 for (int i = 0; i < propertyArrLength; i++) { 1019 Object propertyElement = Array.get(property, i); 1020 if (propertyElement instanceof String) { 1021 builder.append("\"").append((String) propertyElement).append("\""); 1022 } else if (propertyElement instanceof byte[]) { 1023 builder.append(Arrays.toString((byte[]) propertyElement)); 1024 } else if (propertyElement != null) { 1025 builder.append(propertyElement.toString()); 1026 } 1027 if (i != propertyArrLength - 1) { 1028 builder.append(", "); 1029 } 1030 } 1031 } 1032 builder.append("]"); 1033 } 1034 1035 /** 1036 * The builder class for {@link GenericDocument}. 1037 * 1038 * @param <BuilderType> Type of subclass who extends this. 1039 */ 1040 // This builder is specifically designed to be extended by classes deriving from 1041 // GenericDocument. 1042 @SuppressLint("StaticFinalBuilder") 1043 @SuppressWarnings("rawtypes") 1044 public static class Builder<BuilderType extends Builder> { 1045 private final GenericDocumentParcel.Builder mDocumentParcelBuilder; 1046 private final BuilderType mBuilderTypeInstance; 1047 1048 /** 1049 * Creates a new {@link GenericDocument.Builder}. 1050 * 1051 * <p>Document IDs are unique within a namespace. 1052 * 1053 * <p>The number of namespaces per app should be kept small for efficiency reasons. 1054 * 1055 * @param namespace the namespace to set for the {@link GenericDocument}. 1056 * @param id the unique identifier for the {@link GenericDocument} in its namespace. 1057 * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The 1058 * provided {@code schemaType} must be defined using {@link AppSearchSession#setSchema} 1059 * prior to inserting a document of this {@code schemaType} into the AppSearch index 1060 * using {@link AppSearchSession#put}. Otherwise, the document will be rejected by 1061 * {@link AppSearchSession#put} with result code {@link 1062 * AppSearchResult#RESULT_NOT_FOUND}. 1063 */ 1064 @SuppressWarnings("unchecked") Builder(@onNull String namespace, @NonNull String id, @NonNull String schemaType)1065 public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) { 1066 Objects.requireNonNull(namespace); 1067 Objects.requireNonNull(id); 1068 Objects.requireNonNull(schemaType); 1069 1070 mBuilderTypeInstance = (BuilderType) this; 1071 mDocumentParcelBuilder = new GenericDocumentParcel.Builder(namespace, id, schemaType); 1072 } 1073 1074 /** 1075 * Creates a new {@link GenericDocument.Builder} from the given {@link 1076 * GenericDocumentParcel.Builder}. 1077 * 1078 * <p>The bundle is NOT copied. 1079 */ 1080 @SuppressWarnings("unchecked") Builder(@onNull GenericDocumentParcel.Builder documentParcelBuilder)1081 Builder(@NonNull GenericDocumentParcel.Builder documentParcelBuilder) { 1082 mDocumentParcelBuilder = Objects.requireNonNull(documentParcelBuilder); 1083 mBuilderTypeInstance = (BuilderType) this; 1084 } 1085 1086 /** 1087 * Creates a new {@link GenericDocument.Builder} from the given GenericDocument. 1088 * 1089 * <p>The GenericDocument is deep copied, that is, it changes to a new GenericDocument 1090 * returned by this function and will NOT affect the original GenericDocument. 1091 */ 1092 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR) Builder(@onNull GenericDocument document)1093 public Builder(@NonNull GenericDocument document) { 1094 this(new GenericDocumentParcel.Builder(document.mDocumentParcel)); 1095 } 1096 1097 /** 1098 * Sets the app-defined namespace this document resides in, changing the value provided in 1099 * the constructor. No special values are reserved or understood by the infrastructure. 1100 * 1101 * <p>Document IDs are unique within a namespace. 1102 * 1103 * <p>The number of namespaces per app should be kept small for efficiency reasons. 1104 */ 1105 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1106 @CanIgnoreReturnValue 1107 @NonNull setNamespace(@onNull String namespace)1108 public BuilderType setNamespace(@NonNull String namespace) { 1109 Objects.requireNonNull(namespace); 1110 mDocumentParcelBuilder.setNamespace(namespace); 1111 return mBuilderTypeInstance; 1112 } 1113 1114 /** 1115 * Sets the ID of this document, changing the value provided in the constructor. No special 1116 * values are reserved or understood by the infrastructure. 1117 * 1118 * <p>Document IDs are unique within the combination of package, database, and namespace. 1119 * 1120 * <p>Setting a document with a duplicate id will overwrite the original document with the 1121 * new document, enforcing uniqueness within the above constraint. 1122 */ 1123 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1124 @CanIgnoreReturnValue 1125 @NonNull setId(@onNull String id)1126 public BuilderType setId(@NonNull String id) { 1127 Objects.requireNonNull(id); 1128 mDocumentParcelBuilder.setId(id); 1129 return mBuilderTypeInstance; 1130 } 1131 1132 /** 1133 * Sets the schema type of this document, changing the value provided in the constructor. 1134 * 1135 * <p>To successfully index a document, the schema type must match the name of an {@link 1136 * AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}. 1137 */ 1138 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1139 @CanIgnoreReturnValue 1140 @NonNull setSchemaType(@onNull String schemaType)1141 public BuilderType setSchemaType(@NonNull String schemaType) { 1142 Objects.requireNonNull(schemaType); 1143 mDocumentParcelBuilder.setSchemaType(schemaType); 1144 return mBuilderTypeInstance; 1145 } 1146 1147 /** 1148 * Sets the list of parent types of the {@link GenericDocument}'s type. 1149 * 1150 * <p>Child types must appear before parent types in the list. 1151 * 1152 * @hide 1153 */ 1154 @CanIgnoreReturnValue 1155 @NonNull setParentTypes(@onNull List<String> parentTypes)1156 public BuilderType setParentTypes(@NonNull List<String> parentTypes) { 1157 Objects.requireNonNull(parentTypes); 1158 mDocumentParcelBuilder.setParentTypes(parentTypes); 1159 return mBuilderTypeInstance; 1160 } 1161 1162 /** 1163 * Sets the score of the {@link GenericDocument}. 1164 * 1165 * <p>The score is a query-independent measure of the document's quality, relative to other 1166 * {@link GenericDocument} objects of the same {@link AppSearchSchema} type. 1167 * 1168 * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}. 1169 * Documents with higher scores are considered better than documents with lower scores. 1170 * 1171 * <p>Any non-negative integer can be used a score. By default, scores are set to 0. 1172 * 1173 * @param score any non-negative {@code int} representing the document's score. 1174 * @throws IllegalArgumentException if the score is negative. 1175 */ 1176 @CanIgnoreReturnValue 1177 @NonNull setScore(@ntRangefrom = 0, to = Integer.MAX_VALUE) int score)1178 public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) { 1179 if (score < 0) { 1180 throw new IllegalArgumentException("Document score cannot be negative."); 1181 } 1182 mDocumentParcelBuilder.setScore(score); 1183 return mBuilderTypeInstance; 1184 } 1185 1186 /** 1187 * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds. 1188 * 1189 * <p>This should be set using a value obtained from the {@link System#currentTimeMillis} 1190 * time base. 1191 * 1192 * <p>If this method is not called, this will be set to the time the object is built. 1193 * 1194 * @param creationTimestampMillis a creation timestamp in milliseconds. 1195 */ 1196 @CanIgnoreReturnValue 1197 @NonNull setCreationTimestampMillis( @urrentTimeMillisLong long creationTimestampMillis)1198 public BuilderType setCreationTimestampMillis( 1199 @CurrentTimeMillisLong long creationTimestampMillis) { 1200 mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis); 1201 return mBuilderTypeInstance; 1202 } 1203 1204 /** 1205 * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. 1206 * 1207 * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of 1208 * {@code creationTimestampMillis + ttlMillis}, measured in the {@link 1209 * System#currentTimeMillis} time base, the document will be auto-deleted. 1210 * 1211 * <p>The default value is 0, which means the document is permanent and won't be 1212 * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is called. 1213 * 1214 * @param ttlMillis a non-negative duration in milliseconds. 1215 * @throws IllegalArgumentException if ttlMillis is negative. 1216 */ 1217 @CanIgnoreReturnValue 1218 @NonNull setTtlMillis(long ttlMillis)1219 public BuilderType setTtlMillis(long ttlMillis) { 1220 if (ttlMillis < 0) { 1221 throw new IllegalArgumentException("Document ttlMillis cannot be negative."); 1222 } 1223 mDocumentParcelBuilder.setTtlMillis(ttlMillis); 1224 return mBuilderTypeInstance; 1225 } 1226 1227 /** 1228 * Sets one or multiple {@code String} values for a property, replacing its previous values. 1229 * 1230 * @param name the name associated with the {@code values}. Must match the name for this 1231 * property as given in {@link AppSearchSchema.PropertyConfig#getName}. 1232 * @param values the {@code String} values of the property. 1233 * @throws IllegalArgumentException if no values are provided, or if a passed in {@code 1234 * String} is {@code null} or "". 1235 */ 1236 @CanIgnoreReturnValue 1237 @NonNull setPropertyString(@onNull String name, @NonNull String... values)1238 public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) { 1239 Objects.requireNonNull(name); 1240 Objects.requireNonNull(values); 1241 validatePropertyName(name); 1242 for (int i = 0; i < values.length; i++) { 1243 if (values[i] == null) { 1244 throw new IllegalArgumentException("The String at " + i + " is null."); 1245 } 1246 } 1247 mDocumentParcelBuilder.putInPropertyMap(name, values); 1248 return mBuilderTypeInstance; 1249 } 1250 1251 /** 1252 * Sets one or multiple {@code boolean} values for a property, replacing its previous 1253 * values. 1254 * 1255 * @param name the name associated with the {@code values}. Must match the name for this 1256 * property as given in {@link AppSearchSchema.PropertyConfig#getName}. 1257 * @param values the {@code boolean} values of the property. 1258 * @throws IllegalArgumentException if the name is empty or {@code null}. 1259 */ 1260 @CanIgnoreReturnValue 1261 @NonNull setPropertyBoolean(@onNull String name, @NonNull boolean... values)1262 public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) { 1263 Objects.requireNonNull(name); 1264 Objects.requireNonNull(values); 1265 validatePropertyName(name); 1266 mDocumentParcelBuilder.putInPropertyMap(name, values); 1267 return mBuilderTypeInstance; 1268 } 1269 1270 /** 1271 * Sets one or multiple {@code long} values for a property, replacing its previous values. 1272 * 1273 * @param name the name associated with the {@code values}. Must match the name for this 1274 * property as given in {@link AppSearchSchema.PropertyConfig#getName}. 1275 * @param values the {@code long} values of the property. 1276 * @throws IllegalArgumentException if the name is empty or {@code null}. 1277 */ 1278 @CanIgnoreReturnValue 1279 @NonNull setPropertyLong(@onNull String name, @NonNull long... values)1280 public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) { 1281 Objects.requireNonNull(name); 1282 Objects.requireNonNull(values); 1283 validatePropertyName(name); 1284 mDocumentParcelBuilder.putInPropertyMap(name, values); 1285 return mBuilderTypeInstance; 1286 } 1287 1288 /** 1289 * Sets one or multiple {@code double} values for a property, replacing its previous values. 1290 * 1291 * @param name the name associated with the {@code values}. Must match the name for this 1292 * property as given in {@link AppSearchSchema.PropertyConfig#getName}. 1293 * @param values the {@code double} values of the property. 1294 * @throws IllegalArgumentException if the name is empty or {@code null}. 1295 */ 1296 @CanIgnoreReturnValue 1297 @NonNull setPropertyDouble(@onNull String name, @NonNull double... values)1298 public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) { 1299 Objects.requireNonNull(name); 1300 Objects.requireNonNull(values); 1301 validatePropertyName(name); 1302 mDocumentParcelBuilder.putInPropertyMap(name, values); 1303 return mBuilderTypeInstance; 1304 } 1305 1306 /** 1307 * Sets one or multiple {@code byte[]} for a property, replacing its previous values. 1308 * 1309 * @param name the name associated with the {@code values}. Must match the name for this 1310 * property as given in {@link AppSearchSchema.PropertyConfig#getName}. 1311 * @param values the {@code byte[]} of the property. 1312 * @throws IllegalArgumentException if no values are provided, or if a passed in {@code 1313 * byte[]} is {@code null}, or if name is empty. 1314 */ 1315 @CanIgnoreReturnValue 1316 @NonNull setPropertyBytes(@onNull String name, @NonNull byte[]... values)1317 public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) { 1318 Objects.requireNonNull(name); 1319 Objects.requireNonNull(values); 1320 validatePropertyName(name); 1321 for (int i = 0; i < values.length; i++) { 1322 if (values[i] == null) { 1323 throw new IllegalArgumentException("The byte[] at " + i + " is null."); 1324 } 1325 } 1326 mDocumentParcelBuilder.putInPropertyMap(name, values); 1327 return mBuilderTypeInstance; 1328 } 1329 1330 /** 1331 * Sets one or multiple {@link GenericDocument} values for a property, replacing its 1332 * previous values. 1333 * 1334 * @param name the name associated with the {@code values}. Must match the name for this 1335 * property as given in {@link AppSearchSchema.PropertyConfig#getName}. 1336 * @param values the {@link GenericDocument} values of the property. 1337 * @throws IllegalArgumentException if no values are provided, or if a passed in {@link 1338 * GenericDocument} is {@code null}, or if name is empty. 1339 */ 1340 @CanIgnoreReturnValue 1341 @NonNull setPropertyDocument( @onNull String name, @NonNull GenericDocument... values)1342 public BuilderType setPropertyDocument( 1343 @NonNull String name, @NonNull GenericDocument... values) { 1344 Objects.requireNonNull(name); 1345 Objects.requireNonNull(values); 1346 validatePropertyName(name); 1347 GenericDocumentParcel[] documentParcels = new GenericDocumentParcel[values.length]; 1348 for (int i = 0; i < values.length; i++) { 1349 if (values[i] == null) { 1350 throw new IllegalArgumentException("The document at " + i + " is null."); 1351 } 1352 documentParcels[i] = values[i].getDocumentParcel(); 1353 } 1354 mDocumentParcelBuilder.putInPropertyMap(name, documentParcels); 1355 return mBuilderTypeInstance; 1356 } 1357 1358 /** 1359 * Sets one or multiple {@code EmbeddingVector} values for a property, replacing its 1360 * previous values. 1361 * 1362 * @param name the name associated with the {@code values}. Must match the name for this 1363 * property as given in {@link AppSearchSchema.PropertyConfig#getName}. 1364 * @param values the {@code EmbeddingVector} values of the property. 1365 * @throws IllegalArgumentException if the name is empty or {@code null}. 1366 */ 1367 @CanIgnoreReturnValue 1368 @NonNull 1369 @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) setPropertyEmbedding( @onNull String name, @NonNull EmbeddingVector... values)1370 public BuilderType setPropertyEmbedding( 1371 @NonNull String name, @NonNull EmbeddingVector... values) { 1372 Objects.requireNonNull(name); 1373 Objects.requireNonNull(values); 1374 validatePropertyName(name); 1375 for (int i = 0; i < values.length; i++) { 1376 if (values[i] == null) { 1377 throw new IllegalArgumentException("The EmbeddingVector at " + i + " is null."); 1378 } 1379 } 1380 mDocumentParcelBuilder.putInPropertyMap(name, values); 1381 return mBuilderTypeInstance; 1382 } 1383 1384 /** 1385 * Clears the value for the property with the given name. 1386 * 1387 * <p>Note that this method does not support property paths. 1388 * 1389 * <p>You should check for the existence of the property in {@link #getPropertyNames} if you 1390 * need to make sure the property being cleared actually exists. 1391 * 1392 * <p>If the string passed is an invalid or nonexistent property, no error message or 1393 * behavior will be observed. 1394 * 1395 * @param name The name of the property to clear. 1396 */ 1397 @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) 1398 @CanIgnoreReturnValue 1399 @NonNull clearProperty(@onNull String name)1400 public BuilderType clearProperty(@NonNull String name) { 1401 Objects.requireNonNull(name); 1402 mDocumentParcelBuilder.clearProperty(name); 1403 return mBuilderTypeInstance; 1404 } 1405 1406 /** Builds the {@link GenericDocument} object. */ 1407 @NonNull build()1408 public GenericDocument build() { 1409 return new GenericDocument(mDocumentParcelBuilder.build()); 1410 } 1411 1412 /** Method to ensure property names are not blank */ validatePropertyName(@onNull String name)1413 private void validatePropertyName(@NonNull String name) { 1414 if (name.isEmpty()) { 1415 throw new IllegalArgumentException("Property name cannot be blank."); 1416 } 1417 } 1418 } 1419 } 1420