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