• 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.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&lt;Bundle&gt;, 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