1 /*
2  * Copyright 2023 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.safeparcel;
18 
19 import android.annotation.SuppressLint;
20 import android.os.Parcel;
21 import android.os.Parcelable;
22 
23 import androidx.annotation.OptIn;
24 import androidx.annotation.RestrictTo;
25 import androidx.appsearch.annotation.CanIgnoreReturnValue;
26 import androidx.appsearch.annotation.CurrentTimeMillisLong;
27 import androidx.appsearch.app.AppSearchBlobHandle;
28 import androidx.appsearch.app.AppSearchSchema;
29 import androidx.appsearch.app.AppSearchSession;
30 import androidx.appsearch.app.EmbeddingVector;
31 import androidx.appsearch.app.ExperimentalAppSearchApi;
32 import androidx.appsearch.app.GenericDocument;
33 import androidx.collection.ArrayMap;
34 
35 import org.jspecify.annotations.NonNull;
36 import org.jspecify.annotations.Nullable;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.Set;
43 
44 /**
45  * Holds data for a {@link GenericDocument}.
46  *
47  * @exportToFramework:hide
48  */
49 @RestrictTo(RestrictTo.Scope.LIBRARY)
50 @SafeParcelable.Class(creator = "GenericDocumentParcelCreator")
51 // This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.
52 @SuppressLint("BanParcelableUsage")
53 public final class GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable {
54     public static final Parcelable.@NonNull Creator<GenericDocumentParcel> CREATOR =
55             new GenericDocumentParcelCreator();
56 
57     /** The default score of document. */
58     private static final int DEFAULT_SCORE = 0;
59 
60     /** The default time-to-live in millisecond of a document, which is infinity. */
61     private static final long DEFAULT_TTL_MILLIS = 0L;
62 
63     /** Default but invalid value for {@code mCreationTimestampMillis}. */
64     private static final long INVALID_CREATION_TIMESTAMP_MILLIS = -1L;
65 
66     @Field(id = 1, getter = "getNamespace")
67     private final @NonNull String mNamespace;
68 
69     @Field(id = 2, getter = "getId")
70     private final @NonNull String mId;
71 
72     @Field(id = 3, getter = "getSchemaType")
73     private final @NonNull String mSchemaType;
74 
75     @Field(id = 4, getter = "getCreationTimestampMillis")
76     private final long mCreationTimestampMillis;
77 
78     @Field(id = 5, getter = "getTtlMillis")
79     private final long mTtlMillis;
80 
81     @Field(id = 6, getter = "getScore")
82     private final int mScore;
83 
84     /**
85      * Contains all properties in {@link GenericDocument} in a list.
86      *
87      * <p>Unfortunately SafeParcelable doesn't support map type so we have to use a list here.
88      */
89     @Field(id = 7, getter = "getProperties")
90     private final @NonNull List<PropertyParcel> mProperties;
91 
92     /**
93      * Contains all parent properties for this {@link GenericDocument} in a list.
94      *
95      */
96     @Field(id = 8, getter = "getParentTypes")
97     private final @Nullable List<String> mParentTypes;
98 
99     /**
100      * Contains all properties in {@link GenericDocument} to support getting properties via name
101      *
102      * <p>This map is created for quick looking up property by name.
103      */
104     private final @NonNull Map<String, PropertyParcel> mPropertyMap;
105 
106     private @Nullable Integer mHashCode;
107 
108     /**
109      * The constructor taking the property list, and create map internally from this list.
110      *
111      * <p> This will be used in createFromParcel, so creating the property map can not be avoided
112      * in this constructor.
113      */
114     @Constructor
GenericDocumentParcel( @aramid = 1) @onNull String namespace, @Param(id = 2) @NonNull String id, @Param(id = 3) @NonNull String schemaType, @Param(id = 4) long creationTimestampMillis, @Param(id = 5) long ttlMillis, @Param(id = 6) int score, @Param(id = 7) @NonNull List<PropertyParcel> properties, @Param(id = 8) @Nullable List<String> parentTypes)115     GenericDocumentParcel(
116             @Param(id = 1) @NonNull String namespace,
117             @Param(id = 2) @NonNull String id,
118             @Param(id = 3) @NonNull String schemaType,
119             @Param(id = 4) long creationTimestampMillis,
120             @Param(id = 5) long ttlMillis,
121             @Param(id = 6) int score,
122             @Param(id = 7) @NonNull List<PropertyParcel> properties,
123             @Param(id = 8) @Nullable List<String> parentTypes) {
124         this(namespace, id, schemaType, creationTimestampMillis, ttlMillis, score,
125                 properties, createPropertyMapFromPropertyArray(properties), parentTypes);
126     }
127 
128     /**
129      * A constructor taking both property list and property map.
130      *
131      * <p>Caller needs to make sure property list and property map
132      * matches(map is generated from list, or list generated from map).
133      */
GenericDocumentParcel( @onNull String namespace, @NonNull String id, @NonNull String schemaType, long creationTimestampMillis, long ttlMillis, int score, @NonNull List<PropertyParcel> properties, @NonNull Map<String, PropertyParcel> propertyMap, @Nullable List<String> parentTypes)134     GenericDocumentParcel(
135             @NonNull String namespace,
136             @NonNull String id,
137             @NonNull String schemaType,
138             long creationTimestampMillis,
139             long ttlMillis,
140             int score,
141             @NonNull List<PropertyParcel> properties,
142             @NonNull Map<String, PropertyParcel> propertyMap,
143             @Nullable List<String> parentTypes) {
144         mNamespace = Objects.requireNonNull(namespace);
145         mId = Objects.requireNonNull(id);
146         mSchemaType = Objects.requireNonNull(schemaType);
147         mCreationTimestampMillis = creationTimestampMillis;
148         mTtlMillis = ttlMillis;
149         mScore = score;
150         mProperties = Objects.requireNonNull(properties);
151         mPropertyMap = Objects.requireNonNull(propertyMap);
152         mParentTypes = parentTypes;
153     }
154 
155     /** Returns the {@link GenericDocumentParcel} object from the given {@link GenericDocument}. */
fromGenericDocument( @onNull GenericDocument genericDocument)156     public static @NonNull GenericDocumentParcel fromGenericDocument(
157             @NonNull GenericDocument genericDocument) {
158         Objects.requireNonNull(genericDocument);
159         return genericDocument.getDocumentParcel();
160     }
161 
createPropertyMapFromPropertyArray( @onNull List<PropertyParcel> properties)162     private static Map<String, PropertyParcel> createPropertyMapFromPropertyArray(
163             @NonNull List<PropertyParcel> properties) {
164         Objects.requireNonNull(properties);
165         Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.size());
166         for (int i = 0; i < properties.size(); ++i) {
167             PropertyParcel property = properties.get(i);
168             propertyMap.put(property.getPropertyName(), property);
169         }
170         return propertyMap;
171     }
172 
173     /** Returns the unique identifier of the {@link GenericDocument}. */
getId()174     public @NonNull String getId() {
175         return mId;
176     }
177 
178     /** Returns the namespace of the {@link GenericDocument}. */
getNamespace()179     public @NonNull String getNamespace() {
180         return mNamespace;
181     }
182 
183     /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
getSchemaType()184     public @NonNull String getSchemaType() {
185         return mSchemaType;
186     }
187 
188     /** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */
189     @CurrentTimeMillisLong
getCreationTimestampMillis()190     public long getCreationTimestampMillis() {
191         return mCreationTimestampMillis;
192     }
193 
194     /** Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. */
getTtlMillis()195     public long getTtlMillis() {
196         return mTtlMillis;
197     }
198 
199     /** Returns the score of the {@link GenericDocument}. */
getScore()200     public int getScore() {
201         return mScore;
202     }
203 
204     /** Returns the names of all properties defined in this document. */
getPropertyNames()205     public @NonNull Set<String> getPropertyNames() {
206         return mPropertyMap.keySet();
207     }
208 
209     /** Returns all the properties the document has. */
getProperties()210     public @NonNull List<PropertyParcel> getProperties() {
211         return mProperties;
212     }
213 
214     /** Returns the property map the document has. */
getPropertyMap()215     public @NonNull Map<String, PropertyParcel> getPropertyMap() {
216         return mPropertyMap;
217     }
218 
219     /** Returns the list of parent types for the {@link GenericDocument}. */
getParentTypes()220     public @Nullable List<String> getParentTypes() {
221         return mParentTypes;
222     }
223 
224     @Override
equals(@ullable Object other)225     public boolean equals(@Nullable Object other) {
226         if (this == other) {
227             return true;
228         }
229         if (!(other instanceof GenericDocumentParcel)) {
230             return false;
231         }
232         GenericDocumentParcel otherDocument = (GenericDocumentParcel) other;
233         return mNamespace.equals(otherDocument.mNamespace)
234                 && mId.equals(otherDocument.mId)
235                 && mSchemaType.equals(otherDocument.mSchemaType)
236                 && mTtlMillis == otherDocument.mTtlMillis
237                 && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis
238                 && mScore == otherDocument.mScore
239                 && Objects.equals(mProperties, otherDocument.mProperties)
240                 && Objects.equals(mPropertyMap, otherDocument.mPropertyMap)
241                 && Objects.equals(mParentTypes, otherDocument.mParentTypes);
242     }
243 
244     @Override
hashCode()245     public int hashCode() {
246         if (mHashCode == null) {
247             mHashCode = Objects.hash(
248                     mNamespace,
249                     mId,
250                     mSchemaType,
251                     mTtlMillis,
252                     mScore,
253                     mCreationTimestampMillis,
254                     Objects.hashCode(mProperties),
255                     Objects.hashCode(mPropertyMap),
256                     Objects.hashCode(mParentTypes));
257         }
258         return mHashCode;
259     }
260 
261     @Override
writeToParcel(@onNull Parcel dest, int flags)262     public void writeToParcel(@NonNull Parcel dest, int flags) {
263         GenericDocumentParcelCreator.writeToParcel(this, dest, flags);
264     }
265 
266     /** The builder class for {@link GenericDocumentParcel}. */
267     @OptIn(markerClass = ExperimentalAppSearchApi.class)
268     public static final class Builder {
269         private String mNamespace;
270         private String mId;
271         private String mSchemaType;
272         private long mCreationTimestampMillis;
273         private long mTtlMillis;
274         private int mScore;
275         private Map<String, PropertyParcel> mPropertyMap;
276         private @Nullable List<String> mParentTypes;
277 
278         /**
279          * Creates a new {@link GenericDocumentParcel.Builder}.
280          *
281          * <p>Document IDs are unique within a namespace.
282          *
283          * <p>The number of namespaces per app should be kept small for efficiency reasons.
284          */
Builder(@onNull String namespace, @NonNull String id, @NonNull String schemaType)285         public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
286             mNamespace = Objects.requireNonNull(namespace);
287             mId = Objects.requireNonNull(id);
288             mSchemaType = Objects.requireNonNull(schemaType);
289             mCreationTimestampMillis = INVALID_CREATION_TIMESTAMP_MILLIS;
290             mTtlMillis = DEFAULT_TTL_MILLIS;
291             mScore = DEFAULT_SCORE;
292             mPropertyMap = new ArrayMap<>();
293         }
294 
295         /**
296          * Creates a new {@link GenericDocumentParcel.Builder} from the given
297          * {@link GenericDocumentParcel}.
298          */
Builder(@onNull GenericDocumentParcel documentSafeParcel)299         public Builder(@NonNull GenericDocumentParcel documentSafeParcel) {
300             Objects.requireNonNull(documentSafeParcel);
301 
302             mNamespace = documentSafeParcel.mNamespace;
303             mId = documentSafeParcel.mId;
304             mSchemaType = documentSafeParcel.mSchemaType;
305             mCreationTimestampMillis = documentSafeParcel.mCreationTimestampMillis;
306             mTtlMillis = documentSafeParcel.mTtlMillis;
307             mScore = documentSafeParcel.mScore;
308 
309             // Create a shallow copy of the map so we won't change the original one.
310             Map<String, PropertyParcel> propertyMap = documentSafeParcel.mPropertyMap;
311             mPropertyMap = new ArrayMap<>(propertyMap.size());
312             for (PropertyParcel value : propertyMap.values()) {
313                 mPropertyMap.put(value.getPropertyName(), value);
314             }
315 
316             // We don't need to create a shallow copy here, as in the setter for ParentTypes we
317             // will create a new list anyway.
318             mParentTypes = documentSafeParcel.mParentTypes;
319         }
320 
321         /**
322          * Sets the app-defined namespace this document resides in, changing the value provided in
323          * the constructor. No special values are reserved or understood by the infrastructure.
324          *
325          * <p>Document IDs are unique within a namespace.
326          *
327          * <p>The number of namespaces per app should be kept small for efficiency reasons.
328          */
329         @CanIgnoreReturnValue
setNamespace(@onNull String namespace)330         public @NonNull Builder setNamespace(@NonNull String namespace) {
331             Objects.requireNonNull(namespace);
332             mNamespace = namespace;
333             return this;
334         }
335 
336         /**
337          * Sets the ID of this document, changing the value provided in the constructor. No special
338          * values are reserved or understood by the infrastructure.
339          *
340          * <p>Document IDs are unique within a namespace.
341          */
342         @CanIgnoreReturnValue
setId(@onNull String id)343         public @NonNull Builder setId(@NonNull String id) {
344             Objects.requireNonNull(id);
345             mId = id;
346             return this;
347         }
348 
349         /**
350          * Sets the schema type of this document, changing the value provided in the constructor.
351          *
352          * <p>To successfully index a document, the schema type must match the name of an {@link
353          * AppSearchSchema} object previously provided to {@link AppSearchSession#setSchemaAsync}.
354          */
355         @CanIgnoreReturnValue
setSchemaType(@onNull String schemaType)356         public @NonNull Builder setSchemaType(@NonNull String schemaType) {
357             Objects.requireNonNull(schemaType);
358             mSchemaType = schemaType;
359             return this;
360         }
361 
362         /** Sets the score of the parent {@link GenericDocument}. */
363         @CanIgnoreReturnValue
setScore(int score)364         public @NonNull Builder setScore(int score) {
365             mScore = score;
366             return this;
367         }
368 
369         /**
370          * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
371          *
372          * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
373          * time base.
374          *
375          * <p>If this method is not called, this will be set to the time the object is built.
376          *
377          * @param creationTimestampMillis a creation timestamp in milliseconds.
378          */
379         @CanIgnoreReturnValue
setCreationTimestampMillis( @urrentTimeMillisLong long creationTimestampMillis)380         public @NonNull Builder setCreationTimestampMillis(
381                 @CurrentTimeMillisLong long creationTimestampMillis) {
382             mCreationTimestampMillis = creationTimestampMillis;
383             return this;
384         }
385 
386         /**
387          * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
388          *
389          * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
390          * {@code creationTimestampMillis + ttlMillis}, measured in the {@link
391          * System#currentTimeMillis} time base, the document will be auto-deleted.
392          *
393          * <p>The default value is 0, which means the document is permanent and won't be
394          * auto-deleted until the app is uninstalled or {@link AppSearchSession#removeAsync} is
395          * called.
396          *
397          * @param ttlMillis a non-negative duration in milliseconds.
398          * @throws IllegalArgumentException if ttlMillis is negative.
399          */
400         @CanIgnoreReturnValue
setTtlMillis(long ttlMillis)401         public @NonNull Builder setTtlMillis(long ttlMillis) {
402             if (ttlMillis < 0) {
403                 throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
404             }
405             mTtlMillis = ttlMillis;
406             return this;
407         }
408 
409         /**
410          * Sets the list of parent types of the {@link GenericDocument}'s type.
411          *
412          * <p>Child types must appear before parent types in the list.
413          */
414         @CanIgnoreReturnValue
setParentTypes(@ullable List<String> parentTypes)415         public @NonNull Builder setParentTypes(@Nullable List<String> parentTypes) {
416             if (parentTypes == null) {
417                 mParentTypes = null;
418             } else {
419                 mParentTypes = new ArrayList<>(parentTypes);
420             }
421             return this;
422         }
423 
424         /**
425          * Clears the value for the property with the given name.
426          *
427          * <p>Note that this method does not support property paths.
428          *
429          * @param name The name of the property to clear.
430          */
431         @CanIgnoreReturnValue
clearProperty(@onNull String name)432         public @NonNull Builder clearProperty(@NonNull String name) {
433             Objects.requireNonNull(name);
434             mPropertyMap.remove(name);
435             return this;
436         }
437 
438         /** Puts an array of {@link String} in the property map. */
439         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, String @NonNull [] values)440         public @NonNull Builder putInPropertyMap(@NonNull String name, String @NonNull [] values)
441                 throws IllegalArgumentException {
442             putInPropertyMap(name,
443                     new PropertyParcel.Builder(name).setStringValues(values).build());
444             return this;
445         }
446 
447         /** Puts an array of boolean in the property map. */
448         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, boolean @NonNull [] values)449         public @NonNull Builder putInPropertyMap(@NonNull String name, boolean @NonNull [] values) {
450             putInPropertyMap(name,
451                     new PropertyParcel.Builder(name).setBooleanValues(values).build());
452             return this;
453         }
454 
455         /** Puts an array of double in the property map. */
456         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, double @NonNull [] values)457         public @NonNull Builder putInPropertyMap(@NonNull String name, double @NonNull [] values) {
458             putInPropertyMap(name,
459                     new PropertyParcel.Builder(name).setDoubleValues(values).build());
460             return this;
461         }
462 
463         /** Puts an array of long in the property map. */
464         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, long @NonNull [] values)465         public @NonNull Builder putInPropertyMap(@NonNull String name, long @NonNull [] values) {
466             putInPropertyMap(name,
467                     new PropertyParcel.Builder(name).setLongValues(values).build());
468             return this;
469         }
470 
471         /**
472          * Converts and saves a byte[][] into {@link #mProperties}.
473          */
474         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, byte @NonNull [][] values)475         public @NonNull Builder putInPropertyMap(@NonNull String name, byte @NonNull [][] values) {
476             putInPropertyMap(name,
477                     new PropertyParcel.Builder(name).setBytesValues(values).build());
478             return this;
479         }
480 
481         /** Puts an array of {@link GenericDocumentParcel} in the property map. */
482         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, GenericDocumentParcel @NonNull [] values)483         public @NonNull Builder putInPropertyMap(@NonNull String name,
484                 GenericDocumentParcel @NonNull [] values) {
485             putInPropertyMap(name,
486                     new PropertyParcel.Builder(name).setDocumentValues(values).build());
487             return this;
488         }
489 
490         /** Puts an array of {@link EmbeddingVector} in the property map. */
491         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, EmbeddingVector @NonNull [] values)492         public @NonNull Builder putInPropertyMap(@NonNull String name,
493                 EmbeddingVector @NonNull [] values) {
494             putInPropertyMap(name,
495                     new PropertyParcel.Builder(name).setEmbeddingValues(values).build());
496             return this;
497         }
498 
499         /** Puts an array of {@link AppSearchBlobHandle} in the property map. */
500         @CanIgnoreReturnValue
501         @ExperimentalAppSearchApi
putInPropertyMap(@onNull String name, AppSearchBlobHandle @NonNull [] values)502         public @NonNull Builder putInPropertyMap(@NonNull String name,
503                 AppSearchBlobHandle @NonNull [] values) {
504             Objects.requireNonNull(values);
505             putInPropertyMap(name,
506                     new PropertyParcel.Builder(name).setBlobHandleValues(values).build());
507             return this;
508         }
509 
510         /** Directly puts a {@link PropertyParcel} in the property map. */
511         @CanIgnoreReturnValue
putInPropertyMap(@onNull String name, @NonNull PropertyParcel value)512         public @NonNull Builder putInPropertyMap(@NonNull String name,
513                 @NonNull PropertyParcel value) {
514             Objects.requireNonNull(value);
515             mPropertyMap.put(name, value);
516             return this;
517         }
518 
519         /** Builds the {@link GenericDocument} object. */
build()520         public @NonNull GenericDocumentParcel build() {
521             // Set current timestamp for creation timestamp by default.
522             if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) {
523                 mCreationTimestampMillis = System.currentTimeMillis();
524             }
525             return new GenericDocumentParcel(
526                     mNamespace,
527                     mId,
528                     mSchemaType,
529                     mCreationTimestampMillis,
530                     mTtlMillis,
531                     mScore,
532                     new ArrayList<>(mPropertyMap.values()),
533                     mParentTypes);
534         }
535     }
536 }
537