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