/* * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.app.appsearch.safeparcel; import android.annotation.CurrentTimeMillisLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.appsearch.AppSearchSchema; import android.app.appsearch.AppSearchSession; import android.app.appsearch.GenericDocument; import android.app.appsearch.annotation.CanIgnoreReturnValue; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * Holds data for a {@link GenericDocument}. * * @hide */ @SafeParcelable.Class(creator = "GenericDocumentParcelCreator") // This won't be used to send data over binder, and we have to use Parcelable for code sync purpose. @SuppressLint("BanParcelableUsage") public final class GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable { @NonNull public static final Parcelable.Creator CREATOR = new GenericDocumentParcelCreator(); /** The default score of document. */ private static final int DEFAULT_SCORE = 0; /** The default time-to-live in millisecond of a document, which is infinity. */ private static final long DEFAULT_TTL_MILLIS = 0L; /** Default but invalid value for {@code mCreationTimestampMillis}. */ private static final long INVALID_CREATION_TIMESTAMP_MILLIS = -1L; @Field(id = 1, getter = "getNamespace") @NonNull private final String mNamespace; @Field(id = 2, getter = "getId") @NonNull private final String mId; @Field(id = 3, getter = "getSchemaType") @NonNull private final String mSchemaType; @Field(id = 4, getter = "getCreationTimestampMillis") private final long mCreationTimestampMillis; @Field(id = 5, getter = "getTtlMillis") private final long mTtlMillis; @Field(id = 6, getter = "getScore") private final int mScore; /** * Contains all properties in {@link GenericDocument} in a list. * *

Unfortunately SafeParcelable doesn't support map type so we have to use a list here. */ @Field(id = 7, getter = "getProperties") @NonNull private final List mProperties; /** Contains all parent properties for this {@link GenericDocument} in a list. */ @Field(id = 8, getter = "getParentTypes") @Nullable private final List mParentTypes; /** * Contains all properties in {@link GenericDocument} to support getting properties via name * *

This map is created for quick looking up property by name. */ @NonNull private final Map mPropertyMap; @Nullable private Integer mHashCode; /** * The constructor taking the property list, and create map internally from this list. * *

This will be used in createFromParcel, so creating the property map can not be avoided in * this constructor. */ @Constructor GenericDocumentParcel( @Param(id = 1) @NonNull 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 properties, @Param(id = 8) @Nullable List parentTypes) { this( namespace, id, schemaType, creationTimestampMillis, ttlMillis, score, properties, createPropertyMapFromPropertyArray(properties), parentTypes); } /** * A constructor taking both property list and property map. * *

Caller needs to make sure property list and property map matches(map is generated from * list, or list generated from map). */ GenericDocumentParcel( @NonNull String namespace, @NonNull String id, @NonNull String schemaType, long creationTimestampMillis, long ttlMillis, int score, @NonNull List properties, @NonNull Map propertyMap, @Nullable List parentTypes) { mNamespace = Objects.requireNonNull(namespace); mId = Objects.requireNonNull(id); mSchemaType = Objects.requireNonNull(schemaType); mCreationTimestampMillis = creationTimestampMillis; mTtlMillis = ttlMillis; mScore = score; mProperties = Objects.requireNonNull(properties); mPropertyMap = Objects.requireNonNull(propertyMap); mParentTypes = parentTypes; } private static Map createPropertyMapFromPropertyArray( @NonNull List properties) { Objects.requireNonNull(properties); Map propertyMap = new ArrayMap<>(properties.size()); for (int i = 0; i < properties.size(); ++i) { PropertyParcel property = properties.get(i); propertyMap.put(property.getPropertyName(), property); } return propertyMap; } /** Returns the unique identifier of the {@link GenericDocument}. */ @NonNull public String getId() { return mId; } /** Returns the namespace of the {@link GenericDocument}. */ @NonNull public String getNamespace() { return mNamespace; } /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */ @NonNull public String getSchemaType() { return mSchemaType; } /** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */ @CurrentTimeMillisLong public long getCreationTimestampMillis() { return mCreationTimestampMillis; } /** Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. */ public long getTtlMillis() { return mTtlMillis; } /** Returns the score of the {@link GenericDocument}. */ public int getScore() { return mScore; } /** Returns the names of all properties defined in this document. */ @NonNull public Set getPropertyNames() { return mPropertyMap.keySet(); } /** Returns all the properties the document has. */ @NonNull public List getProperties() { return mProperties; } /** Returns the property map the document has. */ @NonNull public Map getPropertyMap() { return mPropertyMap; } /** Returns the list of parent types for the {@link GenericDocument}. */ @Nullable public List getParentTypes() { return mParentTypes; } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof GenericDocumentParcel)) { return false; } GenericDocumentParcel otherDocument = (GenericDocumentParcel) other; return mNamespace.equals(otherDocument.mNamespace) && mId.equals(otherDocument.mId) && mSchemaType.equals(otherDocument.mSchemaType) && mTtlMillis == otherDocument.mTtlMillis && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis && mScore == otherDocument.mScore && Objects.equals(mProperties, otherDocument.mProperties) && Objects.equals(mPropertyMap, otherDocument.mPropertyMap) && Objects.equals(mParentTypes, otherDocument.mParentTypes); } @Override public int hashCode() { if (mHashCode == null) { mHashCode = Objects.hash( mNamespace, mId, mSchemaType, mTtlMillis, mScore, mCreationTimestampMillis, Objects.hashCode(mProperties), Objects.hashCode(mPropertyMap), Objects.hashCode(mParentTypes)); } return mHashCode; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { GenericDocumentParcelCreator.writeToParcel(this, dest, flags); } /** The builder class for {@link GenericDocumentParcel}. */ public static final class Builder { private String mNamespace; private String mId; private String mSchemaType; private long mCreationTimestampMillis; private long mTtlMillis; private int mScore; private Map mPropertyMap; @Nullable private List mParentTypes; /** * Creates a new {@link GenericDocument.Builder}. * *

Document IDs are unique within a namespace. * *

The number of namespaces per app should be kept small for efficiency reasons. */ public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) { mNamespace = Objects.requireNonNull(namespace); mId = Objects.requireNonNull(id); mSchemaType = Objects.requireNonNull(schemaType); mCreationTimestampMillis = INVALID_CREATION_TIMESTAMP_MILLIS; mTtlMillis = DEFAULT_TTL_MILLIS; mScore = DEFAULT_SCORE; mPropertyMap = new ArrayMap<>(); } /** * Creates a new {@link GenericDocumentParcel.Builder} from the given {@link * GenericDocumentParcel}. */ public Builder(@NonNull GenericDocumentParcel documentSafeParcel) { Objects.requireNonNull(documentSafeParcel); mNamespace = documentSafeParcel.mNamespace; mId = documentSafeParcel.mId; mSchemaType = documentSafeParcel.mSchemaType; mCreationTimestampMillis = documentSafeParcel.mCreationTimestampMillis; mTtlMillis = documentSafeParcel.mTtlMillis; mScore = documentSafeParcel.mScore; // Create a shallow copy of the map so we won't change the original one. Map propertyMap = documentSafeParcel.mPropertyMap; mPropertyMap = new ArrayMap<>(propertyMap.size()); for (PropertyParcel value : propertyMap.values()) { mPropertyMap.put(value.getPropertyName(), value); } // We don't need to create a shallow copy here, as in the setter for ParentTypes we // will create a new list anyway. mParentTypes = documentSafeParcel.mParentTypes; } /** * Sets the app-defined namespace this document resides in, changing the value provided in * the constructor. No special values are reserved or understood by the infrastructure. * *

Document IDs are unique within a namespace. * *

The number of namespaces per app should be kept small for efficiency reasons. */ @CanIgnoreReturnValue @NonNull public Builder setNamespace(@NonNull String namespace) { Objects.requireNonNull(namespace); mNamespace = namespace; return this; } /** * Sets the ID of this document, changing the value provided in the constructor. No special * values are reserved or understood by the infrastructure. * *

Document IDs are unique within a namespace. */ @CanIgnoreReturnValue @NonNull public Builder setId(@NonNull String id) { Objects.requireNonNull(id); mId = id; return this; } /** * Sets the schema type of this document, changing the value provided in the constructor. * *

To successfully index a document, the schema type must match the name of an {@link * AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}. */ @CanIgnoreReturnValue @NonNull public Builder setSchemaType(@NonNull String schemaType) { Objects.requireNonNull(schemaType); mSchemaType = schemaType; return this; } /** Sets the score of the parent {@link GenericDocument}. */ @CanIgnoreReturnValue @NonNull public Builder setScore(int score) { mScore = score; return this; } /** * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds. * *

This should be set using a value obtained from the {@link System#currentTimeMillis} * time base. * *

If this method is not called, this will be set to the time the object is built. * * @param creationTimestampMillis a creation timestamp in milliseconds. */ @CanIgnoreReturnValue @NonNull public Builder setCreationTimestampMillis( @CurrentTimeMillisLong long creationTimestampMillis) { mCreationTimestampMillis = creationTimestampMillis; return this; } /** * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. * *

The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of * {@code creationTimestampMillis + ttlMillis}, measured in the {@link * System#currentTimeMillis} time base, the document will be auto-deleted. * *

The default value is 0, which means the document is permanent and won't be * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is called. * * @param ttlMillis a non-negative duration in milliseconds. * @throws IllegalArgumentException if ttlMillis is negative. */ @CanIgnoreReturnValue @NonNull public Builder setTtlMillis(long ttlMillis) { if (ttlMillis < 0) { throw new IllegalArgumentException("Document ttlMillis cannot be negative."); } mTtlMillis = ttlMillis; return this; } /** * Sets the list of parent types of the {@link GenericDocument}'s type. * *

Child types must appear before parent types in the list. */ @CanIgnoreReturnValue @NonNull public Builder setParentTypes(@NonNull List parentTypes) { Objects.requireNonNull(parentTypes); mParentTypes = new ArrayList<>(parentTypes); return this; } /** * Clears the value for the property with the given name. * *

Note that this method does not support property paths. * * @param name The name of the property to clear. */ @CanIgnoreReturnValue @NonNull public Builder clearProperty(@NonNull String name) { Objects.requireNonNull(name); mPropertyMap.remove(name); return this; } /** puts an array of {@link String} in property map. */ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values) throws IllegalArgumentException { putInPropertyMap( name, new PropertyParcel.Builder(name).setStringValues(values).build()); return this; } /** puts an array of boolean in property map. */ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) { putInPropertyMap( name, new PropertyParcel.Builder(name).setBooleanValues(values).build()); return this; } /** puts an array of double in property map. */ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) { putInPropertyMap( name, new PropertyParcel.Builder(name).setDoubleValues(values).build()); return this; } /** puts an array of long in property map. */ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) { putInPropertyMap(name, new PropertyParcel.Builder(name).setLongValues(values).build()); return this; } /** Converts and saves a byte[][] into {@link #mProperties}. */ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) { putInPropertyMap(name, new PropertyParcel.Builder(name).setBytesValues(values).build()); return this; } /** puts an array of {@link GenericDocumentParcel} in property map. */ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap( @NonNull String name, @NonNull GenericDocumentParcel[] values) { putInPropertyMap( name, new PropertyParcel.Builder(name).setDocumentValues(values).build()); return this; } /** Directly puts a {@link PropertyParcel} in property map. */ @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull PropertyParcel value) { Objects.requireNonNull(value); mPropertyMap.put(name, value); return this; } /** Builds the {@link GenericDocument} object. */ @NonNull public GenericDocumentParcel build() { // Set current timestamp for creation timestamp by default. if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) { mCreationTimestampMillis = System.currentTimeMillis(); } return new GenericDocumentParcel( mNamespace, mId, mSchemaType, mCreationTimestampMillis, mTtlMillis, mScore, new ArrayList<>(mPropertyMap.values()), mParentTypes); } } }