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