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