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