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