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 androidx.appsearch.app;
18 
19 import android.annotation.SuppressLint;
20 import android.os.Build;
21 import android.os.Parcel;
22 import android.util.Log;
23 
24 import androidx.annotation.IntRange;
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.annotation.OptIn;
28 import androidx.annotation.RequiresFeature;
29 import androidx.annotation.RestrictTo;
30 import androidx.appsearch.annotation.CanIgnoreReturnValue;
31 import androidx.appsearch.annotation.CurrentTimeMillisLong;
32 import androidx.appsearch.annotation.Document;
33 import androidx.appsearch.annotation.SystemApi;
34 import androidx.appsearch.exceptions.AppSearchException;
35 import androidx.appsearch.flags.FlaggedApi;
36 import androidx.appsearch.flags.Flags;
37 import androidx.appsearch.safeparcel.GenericDocumentParcel;
38 import androidx.appsearch.safeparcel.PropertyParcel;
39 import androidx.appsearch.util.IndentingStringBuilder;
40 import androidx.core.os.ParcelCompat;
41 import androidx.core.util.Preconditions;
42 
43 import java.lang.reflect.Array;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Collections;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Objects;
50 import java.util.Set;
51 
52 /**
53  * Represents a document unit.
54  *
55  * <p>Documents contain structured data conforming to their {@link AppSearchSchema} type.
56  * Each document is uniquely identified by a namespace and a String ID within that namespace.
57  *
58  * <!--@exportToFramework:ifJetpack()-->
59  * <p>Documents are constructed either by using the {@link GenericDocument.Builder} or providing
60  * an annotated {@link Document} data class.
61  * <!--@exportToFramework:else()
62  * <p>Documents are constructed by using the {@link GenericDocument.Builder}.
63  * -->
64  *
65  * @see AppSearchSession#putAsync
66  * @see AppSearchSession#getByDocumentIdAsync
67  * @see AppSearchSession#search
68  */
69 // TODO(b/384721898): Switch to JSpecify annotations
70 @SuppressWarnings("JSpecifyNullness")
71 public class GenericDocument {
72     private static final String TAG = "AppSearchGenericDocumen";
73 
74     /** The maximum number of indexed properties a document can have. */
75     private static final int MAX_INDEXED_PROPERTIES = 16;
76 
77     /**
78      * Fixed constant synthetic property for parent types.
79      *
80      * <!--@exportToFramework:hide-->
81      */
82     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
83     public static final String PARENT_TYPES_SYNTHETIC_PROPERTY = "$$__AppSearch__parentTypes";
84 
85     /**
86      * An immutable empty {@link GenericDocument}.
87      *
88      * <!--@exportToFramework:hide-->
89      */
90     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
91     public static final GenericDocument EMPTY = new GenericDocument.Builder<>("", "", "").build();
92 
93     /**
94      * The maximum number of indexed properties a document can have.
95      *
96      * <p>Indexed properties are properties which are strings where the
97      * {@link AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other
98      * than {@link AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE}, as well as long
99      * properties where the {@link AppSearchSchema.LongPropertyConfig#getIndexingType} value is
100      * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}.
101      *
102      * <!--@exportToFramework:ifJetpack()-->
103      *
104      * @deprecated This is no longer a static value, but depends on SDK version and what AppSearch
105      * implementation is being used. Use {@link Features#getMaxIndexedProperties} instead.
106      * <!--@exportToFramework:else()-->
107      */
108 // @exportToFramework:startStrip()
109     @Deprecated
110 // @exportToFramework:endStrip()
111     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
getMaxIndexedProperties()112     public static int getMaxIndexedProperties() {
113         return MAX_INDEXED_PROPERTIES;
114     }
115 
116 // @exportToFramework:startStrip()
117 
118     /**
119      * Converts an instance of a class annotated with \@{@link Document} into an instance of
120      * {@link GenericDocument}.
121      *
122      * @param document An instance of a class annotated with \@{@link Document}.
123      * @return an instance of {@link GenericDocument} produced by converting {@code document}.
124      * @throws AppSearchException if no generated conversion class exists on the classpath for the
125      *                            given document class or an unexpected error occurs during
126      *                            conversion.
127      * @see GenericDocument#toDocumentClass
128      */
fromDocumentClass(@onNull Object document)129     public static @NonNull GenericDocument fromDocumentClass(@NonNull Object document)
130             throws AppSearchException {
131         Preconditions.checkNotNull(document);
132         DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
133         DocumentClassFactory<Object> factory = registry.getOrCreateFactory(document);
134         return factory.toGenericDocument(document);
135     }
136 // @exportToFramework:endStrip()
137 
138     /** The class to hold all meta data and properties for this {@link GenericDocument}. */
139     private final GenericDocumentParcel mDocumentParcel;
140 
141     /**
142      * Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}.
143      *
144      * @param documentParcel Packaged {@link GenericDocument} data, such as the result of
145      *                       {@link #getDocumentParcel()}.
146      * @exportToFramework:hide
147      */
148     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
149     @SuppressWarnings("deprecation")
GenericDocument(@onNull GenericDocumentParcel documentParcel)150     public GenericDocument(@NonNull GenericDocumentParcel documentParcel) {
151         mDocumentParcel = Objects.requireNonNull(documentParcel);
152     }
153 
154     /**
155      * Creates a new {@link GenericDocument} from an existing instance.
156      *
157      * <p>This method should be only used by constructor of a subclass.
158      */
GenericDocument(@onNull GenericDocument document)159     protected GenericDocument(@NonNull GenericDocument document) {
160         this(document.mDocumentParcel);
161     }
162 
163     /**
164      * Writes the {@link GenericDocument} to the given {@link Parcel}.
165      *
166      * @param dest The {@link Parcel} to write to.
167      * @param flags The flags to use for parceling.
168      * @exportToFramework:hide
169      */
170     // GenericDocument is an open class that can be extended, whereas parcelable classes must be
171     // final in those methods. Thus, we make this a system api to avoid 3p apps depending on it
172     // and getting confused by the inheritability.
173     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
174     @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC)
175     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
writeToParcel(@onNull Parcel dest, int flags)176     public final void writeToParcel(@NonNull Parcel dest, int flags) {
177         Objects.requireNonNull(dest);
178         dest.writeParcelable(mDocumentParcel, flags);
179     }
180 
181     /**
182      * Creates a {@link GenericDocument} from a {@link Parcel}.
183      *
184      * @param parcel The {@link Parcel} to read from.
185      * @exportToFramework:hide
186      */
187     // GenericDocument is an open class that can be extended, whereas parcelable classes must be
188     // final in those methods. Thus, we make this a system api to avoid 3p apps depending on it
189     // and getting confused by the inheritability.
190     @SuppressWarnings("deprecation")
191     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
192     @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_OVER_IPC)
193     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
createFromParcel(@onNull Parcel parcel)194     public static @NonNull GenericDocument createFromParcel(@NonNull Parcel parcel) {
195         Objects.requireNonNull(parcel);
196         GenericDocumentParcel documentParcel;
197         // @exportToFramework:startStrip()
198         if (AppSearchEnvironmentFactory.getEnvironmentInstance().getEnvironment()
199                 != AppSearchEnvironment.FRAMEWORK_ENVIRONMENT) {
200             // Non-Framework code should use ParcelCompat.
201             documentParcel =
202                     ParcelCompat.readParcelable(
203                             parcel, GenericDocumentParcel.class.getClassLoader(),
204                             GenericDocumentParcel.class);
205             return new GenericDocument(documentParcel);
206         }
207         // @exportToFramework:endStrip()
208 
209         // Code built in Framework cannot depend on Androidx libraries. Therefore, we must call
210         // Parcel#readParcelable directly.
211         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
212             documentParcel =
213                     parcel.readParcelable(
214                             GenericDocumentParcel.class.getClassLoader(),
215                             GenericDocumentParcel.class);
216         } else {
217             // The Parcel#readParcelable(ClassLoader, Class) function has a known issue on Android
218             // T. This was fixed on Android U. When on Android T, call the older version of
219             // Parcel#readParcelable.
220             documentParcel =
221                     parcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
222         }
223         return new GenericDocument(documentParcel);
224     }
225 
226     /**
227      * Returns the {@link GenericDocumentParcel} holding the values for this
228      * {@link GenericDocument}.
229      *
230      * @exportToFramework:hide
231      */
232     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
getDocumentParcel()233     public @NonNull GenericDocumentParcel getDocumentParcel() {
234         return mDocumentParcel;
235     }
236 
237     /** Returns the unique identifier of the {@link GenericDocument}. */
getId()238     public @NonNull String getId() {
239         return mDocumentParcel.getId();
240     }
241 
242     /** Returns the namespace of the {@link GenericDocument}. */
getNamespace()243     public @NonNull String getNamespace() {
244         return mDocumentParcel.getNamespace();
245     }
246 
247     /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
getSchemaType()248     public @NonNull String getSchemaType() {
249         return mDocumentParcel.getSchemaType();
250     }
251 
252     /**
253      * Returns the list of parent types of the {@link GenericDocument}'s type.
254      *
255      * <p>It is guaranteed that child types appear before parent types in the list.
256      *
257      * @deprecated Parent types should no longer be set in {@link GenericDocument}. Use
258      * {@link SearchResult.Builder#getParentTypeMap()} instead.
259      * <!--@exportToFramework:hide-->
260      */
261     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
262     @Deprecated
getParentTypes()263     public @Nullable List<String> getParentTypes() {
264         List<String> result = mDocumentParcel.getParentTypes();
265         if (result == null) {
266             return null;
267         }
268         return Collections.unmodifiableList(result);
269     }
270 
271     /**
272      * Returns the creation timestamp of the {@link GenericDocument}, in milliseconds.
273      *
274      * <p>The value is in the {@link System#currentTimeMillis} time base.
275      */
276     @CurrentTimeMillisLong
getCreationTimestampMillis()277     public long getCreationTimestampMillis() {
278         return mDocumentParcel.getCreationTimestampMillis();
279     }
280 
281     /**
282      * Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
283      *
284      * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
285      * {@code creationTimestampMillis + ttlMillis}, measured in the {@link System#currentTimeMillis}
286      * time base, the document will be auto-deleted.
287      *
288      * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
289      * until the app is uninstalled or {@link AppSearchSession#removeAsync} is called.
290      */
getTtlMillis()291     public long getTtlMillis() {
292         return mDocumentParcel.getTtlMillis();
293     }
294 
295     /**
296      * Returns the score of the {@link GenericDocument}.
297      *
298      * <p>The score is a query-independent measure of the document's quality, relative to
299      * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
300      *
301      * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
302      * Documents with higher scores are considered better than documents with lower scores.
303      *
304      * <p>Any non-negative integer can be used a score.
305      */
getScore()306     public int getScore() {
307         return mDocumentParcel.getScore();
308     }
309 
310     /** Returns the names of all properties defined in this document. */
getPropertyNames()311     public @NonNull Set<String> getPropertyNames() {
312         return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames());
313     }
314 
315     /**
316      * Retrieves the property value with the given path as {@link Object}.
317      *
318      * <p>A path can be a simple property name, such as those returned by {@link #getPropertyNames}.
319      * It may also be a dot-delimited path through the nested document hierarchy, with nested
320      * {@link GenericDocument} properties accessed via {@code '.'} and repeated properties
321      * optionally indexed into via {@code [n]}.
322      *
323      * <p>For example, given the following {@link GenericDocument}:
324      * <pre>
325      *     (Message) {
326      *         from: "sender@example.com"
327      *         to: [{
328      *             name: "Albert Einstein"
329      *             email: "einstein@example.com"
330      *           }, {
331      *             name: "Marie Curie"
332      *             email: "curie@example.com"
333      *           }]
334      *         tags: ["important", "inbox"]
335      *         subject: "Hello"
336      *     }
337      * </pre>
338      *
339      * <p>Here are some example paths and their results:
340      * <ul>
341      *     <li>{@code "from"} returns {@code "sender@example.com"} as a {@link String} array with
342      *     one element
343      *     <li>{@code "to"} returns the two nested documents containing contact information as a
344      *     {@link GenericDocument} array with two elements
345      *     <li>{@code "to[1]"} returns the second nested document containing Marie Curie's
346      *     contact information as a {@link GenericDocument} array with one element
347      *     <li>{@code "to[1].email"} returns {@code "curie@example.com"}
348      *     <li>{@code "to[100].email"} returns {@code null} as this particular document does not
349      *     have that many elements in its {@code "to"} array.
350      *     <li>{@code "to.email"} aggregates emails across all nested documents that have them,
351      *     returning {@code ["einstein@example.com", "curie@example.com"]} as a {@link String}
352      *     array with two elements.
353      * </ul>
354      *
355      * <p>If you know the expected type of the property you are retrieving, it is recommended to use
356      * one of the typed versions of this method instead, such as {@link #getPropertyString} or
357      * {@link #getPropertyStringArray}.
358      *
359      * <p>If the property was assigned as an empty array using one of the
360      * {@code Builder#setProperty} functions, this method will return an empty array. If no such
361      * property exists at all, this method returns {@code null}.
362      *
363      * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
364      *   <p>Note: If the property is an empty {@link GenericDocument}[] or {@code byte[][]},
365      *   this method will return a {@code null} value in versions of Android prior to
366      *   {@link android.os.Build.VERSION_CODES#TIRAMISU Android T}. Starting in Android T it will
367      *   return an empty array if the property has been set as an empty array, matching the
368      *   behavior of other property types.
369      * -->
370      *
371      * @param path The path to look for.
372      * @return The entry with the given path as an object or {@code null} if there is no such path.
373      * The returned object will be one of the following types: {@code String[]}, {@code long[]},
374      * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
375      */
getProperty(@onNull String path)376     public @Nullable Object getProperty(@NonNull String path) {
377         Objects.requireNonNull(path);
378         Object rawValue =
379                 getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/ 0,
380                         mDocumentParcel.getPropertyMap());
381 
382         // Unpack the raw value into the types the user expects, if required.
383         if (rawValue instanceof GenericDocumentParcel) {
384             // getRawPropertyFromRawDocument may return a document as a bare documentParcel
385             // as a performance optimization for lookups.
386             GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue);
387             return new GenericDocument[]{document};
388         }
389 
390         if (rawValue instanceof GenericDocumentParcel[]) {
391             // The underlying parcelable of nested GenericDocuments is packed into
392             // a Parcelable array.
393             // We must unpack it into GenericDocument instances.
394             GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue;
395             GenericDocument[] documents = new GenericDocument[docParcels.length];
396             for (int i = 0; i < docParcels.length; i++) {
397                 if (docParcels[i] == null) {
398                     Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path);
399                     continue;
400                 }
401                 documents[i] = new GenericDocument(docParcels[i]);
402             }
403             return documents;
404         }
405 
406         // Otherwise the raw property is the same as the final property and needs no transformation.
407         return rawValue;
408     }
409 
410     /**
411      * Looks up a property path within the given document bundle.
412      *
413      * <p>The return value may be any of GenericDocument's internal repeated storage types
414      * (String[], long[], double[], boolean[], ArrayList&lt;Bundle&gt;, Parcelable[]).
415      *
416      * <p>Usually, this method takes a path and loops over it to get a property from the bundle.
417      * But in the case where we collect documents across repeated nested documents, we need to
418      * recurse back into this method, and so we also keep track of the index into the path.
419      *
420      * @param path        the PropertyPath object representing the path
421      * @param pathIndex   the index into the path we start at
422      * @param propertyMap the map containing the path we are looking up
423      * @return the raw property
424      */
425     @OptIn(markerClass = ExperimentalAppSearchApi.class)
426     @SuppressWarnings("deprecation")
getRawPropertyFromRawDocument( @onNull PropertyPath path, int pathIndex, @NonNull Map<String, PropertyParcel> propertyMap)427     private static @Nullable Object getRawPropertyFromRawDocument(
428             @NonNull PropertyPath path, int pathIndex,
429             @NonNull Map<String, PropertyParcel> propertyMap) {
430         Objects.requireNonNull(path);
431         Objects.requireNonNull(propertyMap);
432         for (int i = pathIndex; i < path.size(); i++) {
433             PropertyPath.PathSegment segment = path.get(i);
434             Object currentElementValue = propertyMap.get(segment.getPropertyName());
435             if (currentElementValue == null) {
436                 return null;
437             }
438 
439             // If the current PathSegment has an index, we now need to update currentElementValue to
440             // contain the value of the indexed property. For example, for a path segment like
441             // "recipients[0]", currentElementValue now contains the value of "recipients" while we
442             // need the value of "recipients[0]".
443             int index = segment.getPropertyIndex();
444             if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) {
445                 // For properties bundle, now we will only get PropertyParcel as the value.
446                 PropertyParcel propertyParcel = (PropertyParcel) currentElementValue;
447 
448                 // Extract the right array element
449                 Object extractedValue = null;
450                 if (propertyParcel.getStringValues() != null) {
451                     String[] stringValues = propertyParcel.getStringValues();
452                     if (stringValues != null && index < stringValues.length) {
453                         extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
454                     }
455                 } else if (propertyParcel.getLongValues() != null) {
456                     long[] longValues = propertyParcel.getLongValues();
457                     if (longValues != null && index < longValues.length) {
458                         extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
459                     }
460                 } else if (propertyParcel.getDoubleValues() != null) {
461                     double[] doubleValues = propertyParcel.getDoubleValues();
462                     if (doubleValues != null && index < doubleValues.length) {
463                         extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
464                     }
465                 } else if (propertyParcel.getBooleanValues() != null) {
466                     boolean[] booleanValues = propertyParcel.getBooleanValues();
467                     if (booleanValues != null && index < booleanValues.length) {
468                         extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
469                     }
470                 } else if (propertyParcel.getBytesValues() != null) {
471                     byte[][] bytesValues = propertyParcel.getBytesValues();
472                     if (bytesValues != null && index < bytesValues.length) {
473                         extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1);
474                     }
475                 } else if (propertyParcel.getDocumentValues() != null) {
476                     // Special optimization: to avoid creating new singleton arrays for traversing
477                     // paths we return the bare document parcel in this particular case.
478                     GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues();
479                     if (docValues != null && index < docValues.length) {
480                         extractedValue = docValues[index];
481                     }
482                 } else if (propertyParcel.getEmbeddingValues() != null) {
483                     EmbeddingVector[] embeddingValues = propertyParcel.getEmbeddingValues();
484                     if (embeddingValues != null && index < embeddingValues.length) {
485                         extractedValue = Arrays.copyOfRange(embeddingValues, index, index + 1);
486                     }
487                 } else if (propertyParcel.getBlobHandleValues() != null) {
488                     AppSearchBlobHandle[] blobHandlesValues = propertyParcel.getBlobHandleValues();
489                     if (blobHandlesValues != null && index < blobHandlesValues.length) {
490                         extractedValue = Arrays.copyOfRange(blobHandlesValues, index, index + 1);
491                     }
492                 } else {
493                     throw new IllegalStateException(
494                             "Unsupported value type: " + currentElementValue);
495                 }
496                 currentElementValue = extractedValue;
497             }
498 
499             // at the end of the path, either something like "...foo" or "...foo[1]"
500             if (currentElementValue == null || i == path.size() - 1) {
501                 if (currentElementValue != null && currentElementValue instanceof PropertyParcel) {
502                     // Unlike previous bundle-based implementation, now each
503                     // value is wrapped in PropertyParcel.
504                     // Here we need to get and return the actual value for non-repeated fields.
505                     currentElementValue = ((PropertyParcel) currentElementValue).getValues();
506                 }
507                 return currentElementValue;
508             }
509 
510             // currentElementValue is now a GenericDocumentParcel or PropertyParcel,
511             // we can continue down the path.
512             if (currentElementValue instanceof GenericDocumentParcel) {
513                 propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap();
514             } else if (currentElementValue instanceof PropertyParcel
515                     && ((PropertyParcel) currentElementValue).getDocumentValues() != null) {
516                 GenericDocumentParcel[] docParcels =
517                         ((PropertyParcel) currentElementValue).getDocumentValues();
518                 if (docParcels != null && docParcels.length == 1) {
519                     propertyMap = docParcels[0].getPropertyMap();
520                     continue;
521                 }
522 
523                 // Slowest path: we're collecting values across repeated nested docs. (Example:
524                 // given a path like recipient.name, where recipient is a repeated field, we return
525                 // a string array where each recipient's name is an array element).
526                 //
527                 // Performance note: Suppose that we have a property path "a.b.c" where the "a"
528                 // property has N document values and each containing a "b" property with M document
529                 // values and each of those containing a "c" property with an int array.
530                 //
531                 // We'll allocate a new ArrayList for each of the "b" properties, add the M int
532                 // arrays from the "c" properties to it and then we'll allocate an int array in
533                 // flattenAccumulator before returning that (1 + M allocation per "b" property).
534                 //
535                 // When we're on the "a" properties, we'll allocate an ArrayList and add the N
536                 // flattened int arrays returned from the "b" properties to the list. Then we'll
537                 // allocate an int array in flattenAccumulator (1 + N ("b" allocs) allocations per
538                 // "a"). // So this implementation could incur 1 + N + NM allocs.
539                 //
540                 // However, we expect the vast majority of getProperty calls to be either for direct
541                 // property names (not paths) or else property paths returned from snippetting,
542                 // which always refer to exactly one property value and don't aggregate across
543                 // repeated values. The implementation is optimized for these two cases, requiring
544                 // no additional allocations. So we've decided that the above performance
545                 // characteristics are OK for the less used path.
546                 if (docParcels != null) {
547                     List<Object> accumulator = new ArrayList<>(docParcels.length);
548                     for (GenericDocumentParcel docParcel : docParcels) {
549                         // recurse as we need to branch
550                         Object value =
551                                 getRawPropertyFromRawDocument(
552                                         path, /*pathIndex=*/ i + 1,
553                                         ((GenericDocumentParcel) docParcel).getPropertyMap());
554                         if (value != null) {
555                             accumulator.add(value);
556                         }
557                     }
558                     // Break the path traversing loop
559                     return flattenAccumulator(accumulator);
560                 }
561             } else {
562                 Log.e(TAG, "Failed to apply path to document; no nested value found: " + path);
563                 return null;
564             }
565         }
566         // Only way to get here is with an empty path list
567         return null;
568     }
569 
570     /**
571      * Combines accumulated repeated properties from multiple documents into a single array.
572      *
573      * @param accumulator List containing objects of the following types: {@code String[]},
574      *                    {@code long[]}, {@code double[]}, {@code boolean[]}, {@code byte[][]},
575      *                    or {@code GenericDocumentParcelable[]}.
576      * @return The result of concatenating each individual list element into a larger array/list of
577      * the same type.
578      */
flattenAccumulator(@onNull List<Object> accumulator)579     private static @Nullable Object flattenAccumulator(@NonNull List<Object> accumulator) {
580         if (accumulator.isEmpty()) {
581             return null;
582         }
583         Object first = accumulator.get(0);
584         if (first instanceof String[]) {
585             int length = 0;
586             for (int i = 0; i < accumulator.size(); i++) {
587                 length += ((String[]) accumulator.get(i)).length;
588             }
589             String[] result = new String[length];
590             int total = 0;
591             for (int i = 0; i < accumulator.size(); i++) {
592                 String[] castValue = (String[]) accumulator.get(i);
593                 System.arraycopy(castValue, 0, result, total, castValue.length);
594                 total += castValue.length;
595             }
596             return result;
597         }
598         if (first instanceof long[]) {
599             int length = 0;
600             for (int i = 0; i < accumulator.size(); i++) {
601                 length += ((long[]) accumulator.get(i)).length;
602             }
603             long[] result = new long[length];
604             int total = 0;
605             for (int i = 0; i < accumulator.size(); i++) {
606                 long[] castValue = (long[]) accumulator.get(i);
607                 System.arraycopy(castValue, 0, result, total, castValue.length);
608                 total += castValue.length;
609             }
610             return result;
611         }
612         if (first instanceof double[]) {
613             int length = 0;
614             for (int i = 0; i < accumulator.size(); i++) {
615                 length += ((double[]) accumulator.get(i)).length;
616             }
617             double[] result = new double[length];
618             int total = 0;
619             for (int i = 0; i < accumulator.size(); i++) {
620                 double[] castValue = (double[]) accumulator.get(i);
621                 System.arraycopy(castValue, 0, result, total, castValue.length);
622                 total += castValue.length;
623             }
624             return result;
625         }
626         if (first instanceof boolean[]) {
627             int length = 0;
628             for (int i = 0; i < accumulator.size(); i++) {
629                 length += ((boolean[]) accumulator.get(i)).length;
630             }
631             boolean[] result = new boolean[length];
632             int total = 0;
633             for (int i = 0; i < accumulator.size(); i++) {
634                 boolean[] castValue = (boolean[]) accumulator.get(i);
635                 System.arraycopy(castValue, 0, result, total, castValue.length);
636                 total += castValue.length;
637             }
638             return result;
639         }
640         if (first instanceof byte[][]) {
641             int length = 0;
642             for (int i = 0; i < accumulator.size(); i++) {
643                 length += ((byte[][]) accumulator.get(i)).length;
644             }
645             byte[][] result = new byte[length][];
646             int total = 0;
647             for (int i = 0; i < accumulator.size(); i++) {
648                 byte[][] castValue = (byte[][]) accumulator.get(i);
649                 System.arraycopy(castValue, 0, result, total, castValue.length);
650                 total += castValue.length;
651             }
652             return result;
653         }
654         if (first instanceof GenericDocumentParcel[]) {
655             int length = 0;
656             for (int i = 0; i < accumulator.size(); i++) {
657                 length += ((GenericDocumentParcel[]) accumulator.get(i)).length;
658             }
659             GenericDocumentParcel[] result = new GenericDocumentParcel[length];
660             int total = 0;
661             for (int i = 0; i < accumulator.size(); i++) {
662                 GenericDocumentParcel[] castValue = (GenericDocumentParcel[]) accumulator.get(i);
663                 System.arraycopy(castValue, 0, result, total, castValue.length);
664                 total += castValue.length;
665             }
666             return result;
667         }
668         throw new IllegalStateException("Unexpected property type: " + first);
669     }
670 
671     /**
672      * Retrieves a {@link String} property by path.
673      *
674      * <p>See {@link #getProperty} for a detailed description of the path syntax.
675      *
676      * @param path The path to look for.
677      * @return The first {@link String} associated with the given path or {@code null} if there is
678      * no such value or the value is of a different type.
679      */
getPropertyString(@onNull String path)680     public @Nullable String getPropertyString(@NonNull String path) {
681         Preconditions.checkNotNull(path);
682         String[] propertyArray = getPropertyStringArray(path);
683         if (propertyArray == null || propertyArray.length == 0) {
684             return null;
685         }
686         warnIfSinglePropertyTooLong("String", path, propertyArray.length);
687         return propertyArray[0];
688     }
689 
690     /**
691      * Retrieves a {@code long} property by path.
692      *
693      * <p>See {@link #getProperty} for a detailed description of the path syntax.
694      *
695      * @param path The path to look for.
696      * @return The first {@code long} associated with the given path or default value {@code 0} if
697      * there is no such value or the value is of a different type.
698      */
getPropertyLong(@onNull String path)699     public long getPropertyLong(@NonNull String path) {
700         Preconditions.checkNotNull(path);
701         long[] propertyArray = getPropertyLongArray(path);
702         if (propertyArray == null || propertyArray.length == 0) {
703             return 0;
704         }
705         warnIfSinglePropertyTooLong("Long", path, propertyArray.length);
706         return propertyArray[0];
707     }
708 
709     /**
710      * Retrieves a {@code double} property by path.
711      *
712      * <p>See {@link #getProperty} for a detailed description of the path syntax.
713      *
714      * @param path The path to look for.
715      * @return The first {@code double} associated with the given path or default value {@code 0.0}
716      * if there is no such value or the value is of a different type.
717      */
getPropertyDouble(@onNull String path)718     public double getPropertyDouble(@NonNull String path) {
719         Preconditions.checkNotNull(path);
720         double[] propertyArray = getPropertyDoubleArray(path);
721         if (propertyArray == null || propertyArray.length == 0) {
722             return 0.0;
723         }
724         warnIfSinglePropertyTooLong("Double", path, propertyArray.length);
725         return propertyArray[0];
726     }
727 
728     /**
729      * Retrieves a {@code boolean} property by path.
730      *
731      * <p>See {@link #getProperty} for a detailed description of the path syntax.
732      *
733      * @param path The path to look for.
734      * @return The first {@code boolean} associated with the given path or default value
735      * {@code false} if there is no such value or the value is of a different type.
736      */
getPropertyBoolean(@onNull String path)737     public boolean getPropertyBoolean(@NonNull String path) {
738         Preconditions.checkNotNull(path);
739         boolean[] propertyArray = getPropertyBooleanArray(path);
740         if (propertyArray == null || propertyArray.length == 0) {
741             return false;
742         }
743         warnIfSinglePropertyTooLong("Boolean", path, propertyArray.length);
744         return propertyArray[0];
745     }
746 
747     /**
748      * Retrieves a {@code byte[]} property by path.
749      *
750      * <p>See {@link #getProperty} for a detailed description of the path syntax.
751      *
752      * @param path The path to look for.
753      * @return The first {@code byte[]} associated with the given path or {@code null} if there is
754      * no such value or the value is of a different type.
755      */
getPropertyBytes(@onNull String path)756     public @Nullable byte[] getPropertyBytes(@NonNull String path) {
757         Preconditions.checkNotNull(path);
758         byte[][] propertyArray = getPropertyBytesArray(path);
759         if (propertyArray == null || propertyArray.length == 0) {
760             return null;
761         }
762         warnIfSinglePropertyTooLong("ByteArray", path, propertyArray.length);
763         return propertyArray[0];
764     }
765 
766     /**
767      * Retrieves a {@link GenericDocument} property by path.
768      *
769      * <p>See {@link #getProperty} for a detailed description of the path syntax.
770      *
771      * @param path The path to look for.
772      * @return The first {@link GenericDocument} associated with the given path or {@code null} if
773      * there is no such value or the value is of a different type.
774      */
getPropertyDocument(@onNull String path)775     public @Nullable GenericDocument getPropertyDocument(@NonNull String path) {
776         Preconditions.checkNotNull(path);
777         GenericDocument[] propertyArray = getPropertyDocumentArray(path);
778         if (propertyArray == null || propertyArray.length == 0) {
779             return null;
780         }
781         warnIfSinglePropertyTooLong("Document", path, propertyArray.length);
782         return propertyArray[0];
783     }
784 
785     /**
786      * Retrieves an {@code EmbeddingVector} property by path.
787      *
788      * <p>See {@link #getProperty} for a detailed description of the path syntax.
789      *
790      * @param path The path to look for.
791      * @return The first {@code EmbeddingVector[]} associated with the given path or
792      * {@code null} if there is no such value or the value is of a different type.
793      */
794     @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
getPropertyEmbedding(@onNull String path)795     public @Nullable EmbeddingVector getPropertyEmbedding(@NonNull String path) {
796         Preconditions.checkNotNull(path);
797         EmbeddingVector[] propertyArray = getPropertyEmbeddingArray(path);
798         if (propertyArray == null || propertyArray.length == 0) {
799             return null;
800         }
801         warnIfSinglePropertyTooLong("Embedding", path, propertyArray.length);
802         return propertyArray[0];
803     }
804 
805     /**
806      * Retrieves an {@link AppSearchBlobHandle} property by path.
807      *
808      * <p>See {@link #getProperty} for a detailed description of the path syntax.
809      *
810      * <p>See {@link AppSearchSession#openBlobForReadAsync} for how to use
811      * {@link AppSearchBlobHandle} to retrieve blob data.
812      *
813      * @param path The path to look for.
814      * @return The first {@link AppSearchBlobHandle} associated with the given path or
815      * {@code null} if there is no such value or the value is of a different type.
816      */
817     @FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
818     @ExperimentalAppSearchApi
getPropertyBlobHandle(@onNull String path)819     public @Nullable AppSearchBlobHandle getPropertyBlobHandle(@NonNull String path) {
820         Preconditions.checkNotNull(path);
821         AppSearchBlobHandle[] propertyArray = getPropertyBlobHandleArray(path);
822         if (propertyArray == null || propertyArray.length == 0) {
823             return null;
824         }
825         warnIfSinglePropertyTooLong("BlobHandle", path, propertyArray.length);
826         return propertyArray[0];
827     }
828 
829     /** Prints a warning to logcat if the given propertyLength is greater than 1. */
warnIfSinglePropertyTooLong( @onNull String propertyType, @NonNull String path, int propertyLength)830     private static void warnIfSinglePropertyTooLong(
831             @NonNull String propertyType, @NonNull String path, int propertyLength) {
832         if (propertyLength > 1) {
833             Log.w(TAG, "The value for \"" + path + "\" contains " + propertyLength
834                     + " elements. Only the first one will be returned from "
835                     + "getProperty" + propertyType + "(). Try getProperty" + propertyType
836                     + "Array().");
837         }
838     }
839 
840     /**
841      * Retrieves a repeated {@code String} property by path.
842      *
843      * <p>See {@link #getProperty} for a detailed description of the path syntax.
844      *
845      * <p>If the property has not been set via {@link Builder#setPropertyString}, this method
846      * returns {@code null}.
847      *
848      * <p>If it has been set via {@link Builder#setPropertyString} to an empty
849      * {@code String[]}, this method returns an empty {@code String[]}.
850      *
851      * @param path The path to look for.
852      * @return The {@code String[]} associated with the given path, or {@code null} if no value is
853      * set or the value is of a different type.
854      */
getPropertyStringArray(@onNull String path)855     public @Nullable String[] getPropertyStringArray(@NonNull String path) {
856         Preconditions.checkNotNull(path);
857         Object value = getProperty(path);
858         return safeCastProperty(path, value, String[].class);
859     }
860 
861     /**
862      * Retrieves a repeated {@code long[]} property by path.
863      *
864      * <p>See {@link #getProperty} for a detailed description of the path syntax.
865      *
866      * <p>If the property has not been set via {@link Builder#setPropertyLong}, this method
867      * returns {@code null}.
868      *
869      * <p>If it has been set via {@link Builder#setPropertyLong} to an empty
870      * {@code long[]}, this method returns an empty {@code long[]}.
871      *
872      * @param path The path to look for.
873      * @return The {@code long[]} associated with the given path, or {@code null} if no value is
874      * set or the value is of a different type.
875      */
getPropertyLongArray(@onNull String path)876     public @Nullable long[] getPropertyLongArray(@NonNull String path) {
877         Preconditions.checkNotNull(path);
878         Object value = getProperty(path);
879         return safeCastProperty(path, value, long[].class);
880     }
881 
882     /**
883      * Retrieves a repeated {@code double} property by path.
884      *
885      * <p>See {@link #getProperty} for a detailed description of the path syntax.
886      *
887      * <p>If the property has not been set via {@link Builder#setPropertyDouble}, this method
888      * returns {@code null}.
889      *
890      * <p>If it has been set via {@link Builder#setPropertyDouble} to an empty
891      * {@code double[]}, this method returns an empty {@code double[]}.
892      *
893      * @param path The path to look for.
894      * @return The {@code double[]} associated with the given path, or {@code null} if no value is
895      * set or the value is of a different type.
896      */
getPropertyDoubleArray(@onNull String path)897     public @Nullable double[] getPropertyDoubleArray(@NonNull String path) {
898         Preconditions.checkNotNull(path);
899         Object value = getProperty(path);
900         return safeCastProperty(path, value, double[].class);
901     }
902 
903     /**
904      * Retrieves a repeated {@code boolean} property by path.
905      *
906      * <p>See {@link #getProperty} for a detailed description of the path syntax.
907      *
908      * <p>If the property has not been set via {@link Builder#setPropertyBoolean}, this method
909      * returns {@code null}.
910      *
911      * <p>If it has been set via {@link Builder#setPropertyBoolean} to an empty
912      * {@code boolean[]}, this method returns an empty {@code boolean[]}.
913      *
914      * @param path The path to look for.
915      * @return The {@code boolean[]} associated with the given path, or {@code null} if no value
916      * is set or the value is of a different type.
917      */
getPropertyBooleanArray(@onNull String path)918     public @Nullable boolean[] getPropertyBooleanArray(@NonNull String path) {
919         Preconditions.checkNotNull(path);
920         Object value = getProperty(path);
921         return safeCastProperty(path, value, boolean[].class);
922     }
923 
924     /**
925      * Retrieves a {@code byte[][]} property by path.
926      *
927      * <p>See {@link #getProperty} for a detailed description of the path syntax.
928      *
929      * <p>If the property has not been set via {@link Builder#setPropertyBytes}, this method
930      * returns {@code null}.
931      *
932      * <!--@exportToFramework:ifJetpack()-->
933      * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
934      * this method returns an empty {@code byte[][]}.
935      * <!--@exportToFramework:else()
936      * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
937      * this method returns an empty {@code byte[][]} starting in
938      * {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
939      * versions of Android.
940      * -->
941      *
942      * @param path The path to look for.
943      * @return The {@code byte[][]} associated with the given path, or {@code null} if no value is
944      * set or the value is of a different type.
945      */
946     @SuppressLint("ArrayReturn")
getPropertyBytesArray(@onNull String path)947     public @Nullable byte[][] getPropertyBytesArray(@NonNull String path) {
948         Preconditions.checkNotNull(path);
949         Object value = getProperty(path);
950         return safeCastProperty(path, value, byte[][].class);
951     }
952 
953     /**
954      * Retrieves a repeated {@link GenericDocument} property by path.
955      *
956      * <p>See {@link #getProperty} for a detailed description of the path syntax.
957      *
958      * <p>If the property has not been set via {@link Builder#setPropertyDocument}, this method
959      * returns {@code null}.
960      *
961      * <!--@exportToFramework:ifJetpack()-->
962      * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
963      * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}.
964      * <!--@exportToFramework:else()
965      * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
966      * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting
967      * in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
968      * versions of Android.
969      * -->
970      *
971      * @param path The path to look for.
972      * @return The {@link GenericDocument}[] associated with the given path, or {@code null} if no
973      * value is set or the value is of a different type.
974      */
975     @SuppressLint("ArrayReturn")
getPropertyDocumentArray(@onNull String path)976     public @Nullable GenericDocument[] getPropertyDocumentArray(@NonNull String path) {
977         Preconditions.checkNotNull(path);
978         Object value = getProperty(path);
979         return safeCastProperty(path, value, GenericDocument[].class);
980     }
981 
982     /**
983      * Retrieves a repeated {@code EmbeddingVector[]} property by path.
984      *
985      * <p>See {@link #getProperty} for a detailed description of the path syntax.
986      *
987      * <p>If the property has not been set via {@link Builder#setPropertyEmbedding}, this method
988      * returns {@code null}.
989      *
990      * <p>If it has been set via {@link Builder#setPropertyEmbedding} to an empty
991      * {@code EmbeddingVector[]}, this method returns an empty
992      * {@code EmbeddingVector[]}.
993      *
994      * @param path The path to look for.
995      * @return The {@code EmbeddingVector[]} associated with the given path, or
996      * {@code null} if no value is set or the value is of a different type.
997      */
998     @SuppressLint({"ArrayReturn", "NullableCollection"})
999     @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
getPropertyEmbeddingArray(@onNull String path)1000     public @Nullable EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String path) {
1001         Preconditions.checkNotNull(path);
1002         Object value = getProperty(path);
1003         return safeCastProperty(path, value, EmbeddingVector[].class);
1004     }
1005 
1006     /**
1007      * Retrieves a repeated {@code AppSearchBlobHandle[]} property by path.
1008      *
1009      * <p>See {@link #getProperty} for a detailed description of the path syntax.
1010      *
1011      * <p>If the property has not been set via {@link Builder#setPropertyBlobHandle}, this method
1012      * returns {@code null}.
1013      *
1014      * <p>If it has been set via {@link Builder#setPropertyBlobHandle} to an empty
1015      * {@code AppSearchBlobHandle[]}, this method returns an empty
1016      * {@code AppSearchBlobHandle[]}.
1017      *
1018      * @param path The path to look for.
1019      * @return The {@code AppSearchBlobHandle[]} associated with the given path, or
1020      * {@code null} if no value is set or the value is of a different type.
1021      */
1022     @SuppressLint({"ArrayReturn", "NullableCollection"})
1023     @ExperimentalAppSearchApi
1024     @FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
getPropertyBlobHandleArray(@onNull String path)1025     public @Nullable AppSearchBlobHandle[] getPropertyBlobHandleArray(@NonNull String path) {
1026         Preconditions.checkNotNull(path);
1027         Object value = getProperty(path);
1028         return safeCastProperty(path, value, AppSearchBlobHandle[].class);
1029     }
1030 
1031     /**
1032      * Casts a repeated property to the provided type, logging an error and returning {@code null}
1033      * if the cast fails.
1034      *
1035      * @param path   Path to the property within the document. Used for logging.
1036      * @param value  Value of the property
1037      * @param tClass Class to cast the value into
1038      */
safeCastProperty( @onNull String path, @Nullable Object value, @NonNull Class<T> tClass)1039     private static <T> @Nullable T safeCastProperty(
1040             @NonNull String path, @Nullable Object value, @NonNull Class<T> tClass) {
1041         if (value == null) {
1042             return null;
1043         }
1044         try {
1045             return tClass.cast(value);
1046         } catch (ClassCastException e) {
1047             Log.w(TAG, "Error casting to requested type for path \"" + path + "\"", e);
1048             return null;
1049         }
1050     }
1051 
1052 // @exportToFramework:startStrip()
1053 
1054     /**
1055      * Converts this GenericDocument into an instance of the provided document class.
1056      *
1057      * <p>It is the developer's responsibility to ensure the right kind of document class is being
1058      * supplied here, either by structuring the application code to ensure the document type is
1059      * known, or by checking the return value of {@link #getSchemaType}.
1060      *
1061      * <p>Document properties are identified by {@code String} names. Any that are found are
1062      * assigned into fields of the given document class. As such, the most likely outcome of
1063      * supplying the wrong document class would be an empty or partially populated result.
1064      *
1065      * @param documentClass a class annotated with {@link Document}
1066      * @return an instance of the document class after being converted from a
1067      * {@link GenericDocument}
1068      * @throws AppSearchException if no factory for this document class could be found on the
1069      *                            classpath.
1070      * @see GenericDocument#fromDocumentClass
1071      */
1072     @OptIn(markerClass = ExperimentalAppSearchApi.class)
toDocumentClass(@onNull Class<T> documentClass)1073     public <T> @NonNull T toDocumentClass(@NonNull Class<T> documentClass)
1074             throws AppSearchException {
1075         return toDocumentClass(documentClass, DocumentClassMappingContext.EMPTY);
1076     }
1077 
1078     /**
1079      * Converts this GenericDocument into an instance of the provided document class.
1080      *
1081      * <p>It is the developer's responsibility to ensure the right kind of document class is being
1082      * supplied here, either by structuring the application code to ensure the document type is
1083      * known, or by checking the return value of {@link #getSchemaType}.
1084      *
1085      * <p>Document properties are identified by {@code String} names. Any that are found are
1086      * assigned into fields of the given document class. As such, the most likely outcome of
1087      * supplying the wrong document class would be an empty or partially populated result.
1088      *
1089      * <p>If this GenericDocument's type is recorded as a subtype of the provided
1090      * {@code documentClass}, the method will find an AppSearch document class, using the provided
1091      * {@code documentClassMappingContext}, that is the most concrete and assignable to
1092      * {@code documentClass}, and then deserialize to that class instead. This allows for more
1093      * specific and accurate deserialization of GenericDocuments. If
1094      * {@code documentClassMappingContext} has information missing or we are not able to find a
1095      * candidate assignable to {@code documentClass}, the method will deserialize to
1096      * {@code documentClass} directly.
1097      *
1098      * <p>Assignability is determined by the programing language's type system, and which type is
1099      * more concrete is determined by AppSearch's type system specified via
1100      * {@link AppSearchSchema.Builder#addParentType(String)} or the annotation parameter
1101      * {@link Document#parent()}.
1102      *
1103      * <p>For nested document properties, this method will be called recursively, and
1104      * {@code documentClassMappingContext} will be passed down to the recursive
1105      * calls of this method.
1106      *
1107      * <p>For most use cases, it is recommended to utilize
1108      * {@link SearchResult#getDocument(Class, Map)} instead of calling this method directly. This
1109      * avoids the need to manually create a {@link DocumentClassMappingContext}.
1110      *
1111      * @param documentClass               a class annotated with {@link Document}
1112      * @param documentClassMappingContext a {@link DocumentClassMappingContext} instance
1113      * @return an instance of the document class after being converted from a
1114      * {@link GenericDocument}
1115      * @throws AppSearchException if no factory for this document class could be found on the
1116      *                            classpath.
1117      * @see GenericDocument#fromDocumentClass
1118      */
1119     @ExperimentalAppSearchApi
toDocumentClass(@onNull Class<T> documentClass, @NonNull DocumentClassMappingContext documentClassMappingContext)1120     public <T> @NonNull T toDocumentClass(@NonNull Class<T> documentClass,
1121             @NonNull DocumentClassMappingContext documentClassMappingContext)
1122             throws AppSearchException {
1123         Preconditions.checkNotNull(documentClass);
1124         Preconditions.checkNotNull(documentClassMappingContext);
1125         DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
1126         Class<? extends T> targetClass = findTargetClassToDeserialize(documentClass,
1127                 documentClassMappingContext.getDocumentClassMap(),
1128                 documentClassMappingContext.getParentTypeMap());
1129         DocumentClassFactory<? extends T> factory = registry.getOrCreateFactory(targetClass);
1130         return factory.fromGenericDocument(this, documentClassMappingContext);
1131     }
1132 
1133     /**
1134      * Find a target class that is assignable to {@code documentClass} to deserialize this
1135      * document, based on the provided {@code documentClassMap} and {@code parentTypeMap}. If
1136      * {@code documentClassMap} is empty, return {@code documentClass} directly.
1137      * If {@code parentTypeMap} does not contain the required parent type information, this
1138      * method will try the deprecated {@link #getParentTypes()}.
1139      *
1140      * <p>This method first tries to find a target class corresponding to the document's own type.
1141      * If that fails, it then tries to find a class corresponding to the document's parent type.
1142      * If that still fails, {@code documentClass} itself will be returned.
1143      */
findTargetClassToDeserialize( @onNull Class<T> documentClass, @NonNull Map<String, List<String>> documentClassMap, @NonNull Map<String, List<String>> parentTypeMap)1144     private <T> @NonNull Class<? extends T> findTargetClassToDeserialize(
1145             @NonNull Class<T> documentClass,
1146             @NonNull Map<String, List<String>> documentClassMap,
1147             @NonNull Map<String, List<String>> parentTypeMap) {
1148         if (documentClassMap.isEmpty()) {
1149             return documentClass;
1150         }
1151 
1152         // Find the target class by the doc's original type.
1153         Class<? extends T> targetClass = AppSearchDocumentClassMap.getAssignableClassBySchemaName(
1154                 documentClassMap, getSchemaType(), documentClass);
1155         if (targetClass != null) {
1156             return targetClass;
1157         }
1158 
1159         // Find the target class by parent types.
1160         List<String> parentTypes;
1161         if (parentTypeMap.containsKey(getSchemaType())) {
1162             parentTypes = parentTypeMap.get(getSchemaType());
1163         } else {
1164             parentTypes = getParentTypes();
1165         }
1166         if (parentTypes != null) {
1167             for (int i = 0; i < parentTypes.size(); ++i) {
1168                 targetClass = AppSearchDocumentClassMap.getAssignableClassBySchemaName(
1169                         documentClassMap, parentTypes.get(i), documentClass);
1170                 if (targetClass != null) {
1171                     return targetClass;
1172                 }
1173             }
1174         }
1175 
1176         Log.w(TAG, "Cannot find any compatible target class to deserialize. Perhaps the annotation "
1177                 + "processor was not run or the generated document class map was proguarded out?\n"
1178                 + "Try to deserialize to " + documentClass.getCanonicalName() + " directly.");
1179         return documentClass;
1180     }
1181 // @exportToFramework:endStrip()
1182 
1183     @Override
equals(@ullable Object other)1184     public boolean equals(@Nullable Object other) {
1185         if (this == other) {
1186             return true;
1187         }
1188         if (!(other instanceof GenericDocument)) {
1189             return false;
1190         }
1191         GenericDocument otherDocument = (GenericDocument) other;
1192         return mDocumentParcel.equals(otherDocument.mDocumentParcel);
1193     }
1194 
1195     @Override
hashCode()1196     public int hashCode() {
1197         return mDocumentParcel.hashCode();
1198     }
1199 
1200     @Override
toString()1201     public @NonNull String toString() {
1202         IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
1203         appendGenericDocumentString(stringBuilder);
1204         return stringBuilder.toString();
1205     }
1206 
1207     /**
1208      * Appends a debug string for the {@link GenericDocument} instance to the given string builder.
1209      *
1210      * @param builder the builder to append to.
1211      */
appendGenericDocumentString(@onNull IndentingStringBuilder builder)1212     void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) {
1213         Preconditions.checkNotNull(builder);
1214 
1215         builder.append("{\n");
1216         builder.increaseIndentLevel();
1217 
1218         builder.append("namespace: \"").append(getNamespace()).append("\",\n");
1219         builder.append("id: \"").append(getId()).append("\",\n");
1220         builder.append("score: ").append(getScore()).append(",\n");
1221         builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
1222         List<String> parentTypes = getParentTypes();
1223         if (parentTypes != null) {
1224             builder.append("parentTypes: ").append(parentTypes).append("\n");
1225         }
1226         builder
1227                 .append("creationTimestampMillis: ")
1228                 .append(getCreationTimestampMillis())
1229                 .append(",\n");
1230         builder.append("timeToLiveMillis: ").append(getTtlMillis()).append(",\n");
1231 
1232         builder.append("properties: {\n");
1233 
1234         String[] sortedProperties = getPropertyNames().toArray(new String[0]);
1235         Arrays.sort(sortedProperties);
1236 
1237         for (int i = 0; i < sortedProperties.length; i++) {
1238             Object property = Preconditions.checkNotNull(getProperty(sortedProperties[i]));
1239             builder.increaseIndentLevel();
1240             appendPropertyString(sortedProperties[i], property, builder);
1241             if (i != sortedProperties.length - 1) {
1242                 builder.append(",\n");
1243             }
1244             builder.decreaseIndentLevel();
1245         }
1246 
1247         builder.append("\n");
1248         builder.append("}");
1249 
1250         builder.decreaseIndentLevel();
1251         builder.append("\n");
1252         builder.append("}");
1253     }
1254 
1255     /**
1256      * Appends a debug string for the given document property to the given string builder.
1257      *
1258      * @param propertyName name of property to create string for.
1259      * @param property     property object to create string for.
1260      * @param builder      the builder to append to.
1261      */
appendPropertyString(@onNull String propertyName, @NonNull Object property, @NonNull IndentingStringBuilder builder)1262     private void appendPropertyString(@NonNull String propertyName, @NonNull Object property,
1263             @NonNull IndentingStringBuilder builder) {
1264         Preconditions.checkNotNull(propertyName);
1265         Preconditions.checkNotNull(property);
1266         Preconditions.checkNotNull(builder);
1267 
1268         builder.append("\"").append(propertyName).append("\": [");
1269         if (property instanceof GenericDocument[]) {
1270             GenericDocument[] documentValues = (GenericDocument[]) property;
1271             for (int i = 0; i < documentValues.length; ++i) {
1272                 builder.append("\n");
1273                 builder.increaseIndentLevel();
1274                 documentValues[i].appendGenericDocumentString(builder);
1275                 if (i != documentValues.length - 1) {
1276                     builder.append(",");
1277                 }
1278                 builder.append("\n");
1279                 builder.decreaseIndentLevel();
1280             }
1281         } else {
1282             int propertyArrLength = Array.getLength(property);
1283             for (int i = 0; i < propertyArrLength; i++) {
1284                 Object propertyElement = Array.get(property, i);
1285                 if (propertyElement instanceof String) {
1286                     builder.append("\"").append((String) propertyElement).append("\"");
1287                 } else if (propertyElement instanceof byte[]) {
1288                     builder.append(Arrays.toString((byte[]) propertyElement));
1289                 } else if (propertyElement != null) {
1290                     builder.append(propertyElement.toString());
1291                 }
1292                 if (i != propertyArrLength - 1) {
1293                     builder.append(", ");
1294                 }
1295             }
1296         }
1297         builder.append("]");
1298     }
1299 
1300     /**
1301      * The builder class for {@link GenericDocument}.
1302      *
1303      * @param <BuilderType> Type of subclass who extends this.
1304      */
1305     // This builder is specifically designed to be extended by classes deriving from
1306     // GenericDocument.
1307     @SuppressLint("StaticFinalBuilder")
1308     @SuppressWarnings("rawtypes")
1309     public static class Builder<BuilderType extends Builder> {
1310         private final GenericDocumentParcel.Builder mDocumentParcelBuilder;
1311         private final BuilderType mBuilderTypeInstance;
1312 
1313         /**
1314          * Creates a new {@link GenericDocument.Builder}.
1315          *
1316          * <p>Document IDs are unique within a namespace.
1317          *
1318          * <p>The number of namespaces per app should be kept small for efficiency reasons.
1319          *
1320          * @param namespace  the namespace to set for the {@link GenericDocument}.
1321          * @param id         the unique identifier for the {@link GenericDocument} in its namespace.
1322          * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
1323          *                   provided {@code schemaType} must be defined using
1324          *                   {@link AppSearchSession#setSchemaAsync} prior
1325          *                   to inserting a document of this {@code schemaType} into the
1326          *                   AppSearch index using
1327          *                   {@link AppSearchSession#putAsync}.
1328          *                   Otherwise, the document will be rejected by
1329          *                   {@link AppSearchSession#putAsync} with result code
1330          *                   {@link AppSearchResult#RESULT_NOT_FOUND}.
1331          */
1332         @SuppressWarnings("unchecked")
Builder(@onNull String namespace, @NonNull String id, @NonNull String schemaType)1333         public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
1334             Preconditions.checkNotNull(namespace);
1335             Preconditions.checkNotNull(id);
1336             Preconditions.checkNotNull(schemaType);
1337 
1338             mBuilderTypeInstance = (BuilderType) this;
1339             mDocumentParcelBuilder = new GenericDocumentParcel.Builder(namespace, id, schemaType);
1340         }
1341 
1342         /**
1343          * Creates a new {@link GenericDocument.Builder} from the given
1344          * {@link GenericDocumentParcel.Builder}.
1345          *
1346          * <p>The bundle is NOT copied.
1347          */
1348         @SuppressWarnings("unchecked")
Builder(@onNull GenericDocumentParcel.Builder documentParcelBuilder)1349         Builder(@NonNull GenericDocumentParcel.Builder documentParcelBuilder) {
1350             mDocumentParcelBuilder = Objects.requireNonNull(documentParcelBuilder);
1351             mBuilderTypeInstance = (BuilderType) this;
1352         }
1353 
1354         /**
1355          * Creates a new {@link GenericDocument.Builder} from the given GenericDocument.
1356          *
1357          * <p>The GenericDocument is deep copied, that is, it changes to a new GenericDocument
1358          * returned by this function and will NOT affect the original GenericDocument.
1359          */
1360         @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR)
Builder(@onNull GenericDocument document)1361         public Builder(@NonNull GenericDocument document) {
1362             this(new GenericDocumentParcel.Builder(document.mDocumentParcel));
1363         }
1364 
1365         /**
1366          * Sets the app-defined namespace this document resides in, changing the value provided
1367          * in the constructor. No special values are reserved or understood by the infrastructure.
1368          *
1369          * <p>Document IDs are unique within a namespace.
1370          *
1371          * <p>The number of namespaces per app should be kept small for efficiency reasons.
1372          */
1373         @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
1374         @CanIgnoreReturnValue
setNamespace(@onNull String namespace)1375         public @NonNull BuilderType setNamespace(@NonNull String namespace) {
1376             Preconditions.checkNotNull(namespace);
1377             mDocumentParcelBuilder.setNamespace(namespace);
1378             return mBuilderTypeInstance;
1379         }
1380 
1381         /**
1382          * Sets the ID of this document, changing the value provided in the constructor. No
1383          * special values are reserved or understood by the infrastructure.
1384          *
1385          * <p>Document IDs are unique within the combination of package, database, and namespace.
1386          *
1387          * <p>Setting a document with a duplicate id will overwrite the original document with
1388          * the new document, enforcing uniqueness within the above constraint.
1389          */
1390         @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
1391         @CanIgnoreReturnValue
setId(@onNull String id)1392         public @NonNull BuilderType setId(@NonNull String id) {
1393             Preconditions.checkNotNull(id);
1394             mDocumentParcelBuilder.setId(id);
1395             return mBuilderTypeInstance;
1396         }
1397 
1398         /**
1399          * Sets the schema type of this document, changing the value provided in the constructor.
1400          *
1401          * <p>To successfully index a document, the schema type must match the name of an
1402          * {@link AppSearchSchema} object previously provided to
1403          * {@link AppSearchSession#setSchemaAsync}.
1404          */
1405         @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
1406         @CanIgnoreReturnValue
setSchemaType(@onNull String schemaType)1407         public @NonNull BuilderType setSchemaType(@NonNull String schemaType) {
1408             Preconditions.checkNotNull(schemaType);
1409             mDocumentParcelBuilder.setSchemaType(schemaType);
1410             return mBuilderTypeInstance;
1411         }
1412 
1413         /**
1414          * Sets the list of parent types of the {@link GenericDocument}'s type.
1415          *
1416          * <p>Child types must appear before parent types in the list.
1417          *
1418          * @deprecated Parent types should no longer be set in {@link GenericDocument}. Use
1419          * {@link SearchResult.Builder#setParentTypeMap(Map)} instead.
1420          * <!--@exportToFramework:hide-->
1421          */
1422         @CanIgnoreReturnValue
1423         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
1424         @Deprecated
setParentTypes(@ullable List<String> parentTypes)1425         public @NonNull BuilderType setParentTypes(@Nullable List<String> parentTypes) {
1426             mDocumentParcelBuilder.setParentTypes(parentTypes);
1427             return mBuilderTypeInstance;
1428         }
1429 
1430         /**
1431          * Sets the score of the {@link GenericDocument}.
1432          *
1433          * <p>The score is a query-independent measure of the document's quality, relative to
1434          * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
1435          *
1436          * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
1437          * Documents with higher scores are considered better than documents with lower scores.
1438          *
1439          * <p>Any non-negative integer can be used a score. By default, scores are set to 0.
1440          *
1441          * @param score any non-negative {@code int} representing the document's score.
1442          * @throws IllegalArgumentException if the score is negative.
1443          */
1444         @CanIgnoreReturnValue
setScore( @ntRangefrom = 0, to = Integer.MAX_VALUE) int score)1445         public @NonNull BuilderType setScore(
1446                 @IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
1447             if (score < 0) {
1448                 throw new IllegalArgumentException("Document score cannot be negative.");
1449             }
1450             mDocumentParcelBuilder.setScore(score);
1451             return mBuilderTypeInstance;
1452         }
1453 
1454         /**
1455          * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
1456          *
1457          * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
1458          * time base.
1459          *
1460          * <p>If this method is not called, this will be set to the time the object is built.
1461          *
1462          * @param creationTimestampMillis a creation timestamp in milliseconds.
1463          */
1464         @CanIgnoreReturnValue
setCreationTimestampMillis( @urrentTimeMillisLong long creationTimestampMillis)1465         public @NonNull BuilderType setCreationTimestampMillis(
1466                 @CurrentTimeMillisLong long creationTimestampMillis) {
1467             mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis);
1468             return mBuilderTypeInstance;
1469         }
1470 
1471         /**
1472          * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
1473          *
1474          * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
1475          * {@code creationTimestampMillis + ttlMillis}, measured in the
1476          * {@link System#currentTimeMillis} time base, the document will be auto-deleted.
1477          *
1478          * <p>The default value is 0, which means the document is permanent and won't be
1479          * auto-deleted until the app is uninstalled or {@link AppSearchSession#removeAsync} is
1480          * called.
1481          *
1482          * @param ttlMillis a non-negative duration in milliseconds.
1483          * @throws IllegalArgumentException if ttlMillis is negative.
1484          */
1485         @CanIgnoreReturnValue
setTtlMillis(long ttlMillis)1486         public @NonNull BuilderType setTtlMillis(long ttlMillis) {
1487             if (ttlMillis < 0) {
1488                 throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
1489             }
1490             mDocumentParcelBuilder.setTtlMillis(ttlMillis);
1491             return mBuilderTypeInstance;
1492         }
1493 
1494         /**
1495          * Sets one or multiple {@code String} values for a property, replacing its previous
1496          * values.
1497          *
1498          * @param name   the name associated with the {@code values}. Must match the name
1499          *               for this property as given in
1500          *               {@link AppSearchSchema.PropertyConfig#getName}.
1501          * @param values the {@code String} values of the property.
1502          * @throws IllegalArgumentException if no values are provided, or if a passed in
1503          *                                  {@code String} is {@code null} or "".
1504          */
1505         @CanIgnoreReturnValue
setPropertyString(@onNull String name, @NonNull String... values)1506         public @NonNull BuilderType setPropertyString(@NonNull String name,
1507                 @NonNull String... values) {
1508             Preconditions.checkNotNull(name);
1509             Preconditions.checkNotNull(values);
1510             validatePropertyName(name);
1511             for (int i = 0; i < values.length; i++) {
1512                 if (values[i] == null) {
1513                     throw new IllegalArgumentException("The String at " + i + " is null.");
1514                 }
1515             }
1516             mDocumentParcelBuilder.putInPropertyMap(name, values);
1517             return mBuilderTypeInstance;
1518         }
1519 
1520         /**
1521          * Sets one or multiple {@code boolean} values for a property, replacing its previous
1522          * values.
1523          *
1524          * @param name   the name associated with the {@code values}. Must match the name
1525          *               for this property as given in
1526          *               {@link AppSearchSchema.PropertyConfig#getName}.
1527          * @param values the {@code boolean} values of the property.
1528          * @throws IllegalArgumentException if the name is empty or {@code null}.
1529          */
1530         @CanIgnoreReturnValue
setPropertyBoolean(@onNull String name, @NonNull boolean... values)1531         public @NonNull BuilderType setPropertyBoolean(@NonNull String name,
1532                 @NonNull boolean... values) {
1533             Preconditions.checkNotNull(name);
1534             Preconditions.checkNotNull(values);
1535             validatePropertyName(name);
1536             mDocumentParcelBuilder.putInPropertyMap(name, values);
1537             return mBuilderTypeInstance;
1538         }
1539 
1540         /**
1541          * Sets one or multiple {@code long} values for a property, replacing its previous
1542          * values.
1543          *
1544          * @param name   the name associated with the {@code values}. Must match the name
1545          *               for this property as given in
1546          *               {@link AppSearchSchema.PropertyConfig#getName}.
1547          * @param values the {@code long} values of the property.
1548          * @throws IllegalArgumentException if the name is empty or {@code null}.
1549          */
1550         @CanIgnoreReturnValue
setPropertyLong(@onNull String name, @NonNull long... values)1551         public @NonNull BuilderType setPropertyLong(@NonNull String name,
1552                 @NonNull long... values) {
1553             Preconditions.checkNotNull(name);
1554             Preconditions.checkNotNull(values);
1555             validatePropertyName(name);
1556             mDocumentParcelBuilder.putInPropertyMap(name, values);
1557             return mBuilderTypeInstance;
1558         }
1559 
1560         /**
1561          * Sets one or multiple {@code double} values for a property, replacing its previous
1562          * values.
1563          *
1564          * @param name   the name associated with the {@code values}. Must match the name
1565          *               for this property as given in
1566          *               {@link AppSearchSchema.PropertyConfig#getName}.
1567          * @param values the {@code double} values of the property.
1568          * @throws IllegalArgumentException if the name is empty or {@code null}.
1569          */
1570         @CanIgnoreReturnValue
setPropertyDouble(@onNull String name, @NonNull double... values)1571         public @NonNull BuilderType setPropertyDouble(@NonNull String name,
1572                 @NonNull double... values) {
1573             Preconditions.checkNotNull(name);
1574             Preconditions.checkNotNull(values);
1575             validatePropertyName(name);
1576             mDocumentParcelBuilder.putInPropertyMap(name, values);
1577             return mBuilderTypeInstance;
1578         }
1579 
1580         /**
1581          * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
1582          *
1583          * <p> For large byte data and lazy retrieval, see {@link #setPropertyBlobHandle}.
1584          *
1585          * @param name   the name associated with the {@code values}. Must match the name
1586          *               for this property as given in
1587          *               {@link AppSearchSchema.PropertyConfig#getName}.
1588          * @param values the {@code byte[]} of the property.
1589          * @throws IllegalArgumentException if no values are provided, or if a passed in
1590          *                                  {@code byte[]} is {@code null}, or if name is empty.
1591          */
1592         @CanIgnoreReturnValue
setPropertyBytes(@onNull String name, @NonNull byte[]... values)1593         public @NonNull BuilderType setPropertyBytes(@NonNull String name,
1594                 @NonNull byte[]... values) {
1595             Preconditions.checkNotNull(name);
1596             Preconditions.checkNotNull(values);
1597             validatePropertyName(name);
1598             for (int i = 0; i < values.length; i++) {
1599                 if (values[i] == null) {
1600                     throw new IllegalArgumentException("The byte[] at " + i + " is null.");
1601                 }
1602             }
1603             mDocumentParcelBuilder.putInPropertyMap(name, values);
1604             return mBuilderTypeInstance;
1605         }
1606 
1607         /**
1608          * Sets one or multiple {@link GenericDocument} values for a property, replacing its
1609          * previous values.
1610          *
1611          * @param name   the name associated with the {@code values}. Must match the name
1612          *               for this property as given in
1613          *               {@link AppSearchSchema.PropertyConfig#getName}.
1614          * @param values the {@link GenericDocument} values of the property.
1615          * @throws IllegalArgumentException if no values are provided, or if a passed in
1616          *                                  {@link GenericDocument} is {@code null}, or if name
1617          *                                  is empty.
1618          */
1619         @CanIgnoreReturnValue
setPropertyDocument( @onNull String name, @NonNull GenericDocument... values)1620         public @NonNull BuilderType setPropertyDocument(
1621                 @NonNull String name, @NonNull GenericDocument... values) {
1622             Preconditions.checkNotNull(name);
1623             Preconditions.checkNotNull(values);
1624             validatePropertyName(name);
1625             GenericDocumentParcel[] documentParcels = new GenericDocumentParcel[values.length];
1626             for (int i = 0; i < values.length; i++) {
1627                 if (values[i] == null) {
1628                     throw new IllegalArgumentException("The document at " + i + " is null.");
1629                 }
1630                 documentParcels[i] = values[i].getDocumentParcel();
1631             }
1632             mDocumentParcelBuilder.putInPropertyMap(name, documentParcels);
1633             return mBuilderTypeInstance;
1634         }
1635 
1636         /**
1637          * Sets one or multiple {@code EmbeddingVector} values for a property, replacing
1638          * its previous values.
1639          *
1640          * @param name   the name associated with the {@code values}. Must match the name
1641          *               for this property as given in
1642          *               {@link AppSearchSchema.PropertyConfig#getName}.
1643          * @param values the {@code EmbeddingVector} values of the property.
1644          * @throws IllegalArgumentException if the name is empty or {@code null}.
1645          */
1646         @CanIgnoreReturnValue
1647         @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
1648         @RequiresFeature(
1649                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
1650                 name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
setPropertyEmbedding(@onNull String name, @NonNull EmbeddingVector... values)1651         public @NonNull BuilderType setPropertyEmbedding(@NonNull String name,
1652                 @NonNull EmbeddingVector... values) {
1653             Preconditions.checkNotNull(name);
1654             Preconditions.checkNotNull(values);
1655             validatePropertyName(name);
1656             for (int i = 0; i < values.length; i++) {
1657                 if (values[i] == null) {
1658                     throw new IllegalArgumentException(
1659                             "The EmbeddingVector at " + i + " is null.");
1660                 }
1661             }
1662             mDocumentParcelBuilder.putInPropertyMap(name, values);
1663             return mBuilderTypeInstance;
1664         }
1665 
1666         /**
1667          * Sets one or multiple {@link AppSearchBlobHandle} values for a property, replacing
1668          * its previous values.
1669          *
1670          * <p>{@link AppSearchBlobHandle} is a pointer to a blob of data.
1671          *
1672          * <p>Store large byte via the {@link android.os.ParcelFileDescriptor} returned from
1673          * {@link AppSearchSession#openBlobForWriteAsync}. Once the blob data is committed via
1674          * {@link AppSearchSession#commitBlobAsync}, the blob is retrievable via
1675          * {@link AppSearchSession#openBlobForReadAsync}.
1676          *
1677          * @param name   the name associated with the {@code values}. Must match the name
1678          *               for this property as given in
1679          *               {@link AppSearchSchema.PropertyConfig#getName}.
1680          * @param values the {@link AppSearchBlobHandle} values of the property.
1681          * @throws IllegalArgumentException if the name is empty or {@code null}.
1682          */
1683         @CanIgnoreReturnValue
1684         @FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
1685         @ExperimentalAppSearchApi
1686         @RequiresFeature(
1687                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
1688                 name = Features.BLOB_STORAGE)
setPropertyBlobHandle(@onNull String name, @NonNull AppSearchBlobHandle... values)1689         public @NonNull BuilderType setPropertyBlobHandle(@NonNull String name,
1690                 @NonNull AppSearchBlobHandle... values) {
1691             Preconditions.checkNotNull(name);
1692             Preconditions.checkNotNull(values);
1693             validatePropertyName(name);
1694             for (int i = 0; i < values.length; i++) {
1695                 if (values[i] == null) {
1696                     throw new IllegalArgumentException(
1697                             "The BlobHandle at " + i + " is null.");
1698                 }
1699             }
1700             mDocumentParcelBuilder.putInPropertyMap(name, values);
1701             return mBuilderTypeInstance;
1702         }
1703 
1704         /**
1705          * Clears the value for the property with the given name.
1706          *
1707          * <p>Note that this method does not support property paths.
1708          *
1709          * <p>You should check for the existence of the property in {@link #getPropertyNames} if
1710          * you need to make sure the property being cleared actually exists.
1711          *
1712          * <p>If the string passed is an invalid or nonexistent property, no error message or
1713          * behavior will be observed.
1714          *
1715          * @param name The name of the property to clear.
1716          */
1717         @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
1718         @CanIgnoreReturnValue
clearProperty(@onNull String name)1719         public @NonNull BuilderType clearProperty(@NonNull String name) {
1720             Preconditions.checkNotNull(name);
1721             mDocumentParcelBuilder.clearProperty(name);
1722             return mBuilderTypeInstance;
1723         }
1724 
1725         /** Builds the {@link GenericDocument} object. */
build()1726         public @NonNull GenericDocument build() {
1727             return new GenericDocument(mDocumentParcelBuilder.build());
1728         }
1729 
1730         /** Method to ensure property names are not blank */
validatePropertyName(@onNull String name)1731         private void validatePropertyName(@NonNull String name) {
1732             if (name.isEmpty()) {
1733                 throw new IllegalArgumentException("Property name cannot be blank.");
1734             }
1735         }
1736     }
1737 }
1738