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