Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d0b1e105 authored by Terry Wang's avatar Terry Wang
Browse files

Adds ttl and support for index byte array in AppSearch.Document.

Bug: 143789408
Test: atest FrameworksCoreTests:android.app.appsearch
Change-Id: Ic3daf03ae232c0142d126c49e7e3ae7201076770
parent 7cc0c30d
Loading
Loading
Loading
Loading
+141 −63
Original line number Diff line number Diff line
@@ -17,33 +17,33 @@
package android.app.appsearch;

import android.annotation.CurrentTimeMillisLong;
import android.annotation.DurationMillisLong;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;

import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.PropertyProto;
import com.google.android.icing.protobuf.ByteString;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Represents a document unit.
 *
 * <p>Documents are constructed via {@link AppSearchDocument.Builder}.
 *
 * @hide
 */
// TODO(b/143789408) set TTL for document in mProtoBuilder
// TODO(b/144518768) add visibility field if the stakeholders are comfortable with a no-op
//  opt-in for this release.
public class AppSearchDocument {
    private static final String TAG = "AppSearchDocument";

@@ -66,20 +66,21 @@ public class AppSearchDocument {
    @NonNull
    private final DocumentProto mProto;

    /** Contains all properties in {@link #mProto} to support get properties via keys. */
    /** Contains all properties in {@link #mProto} to support getting properties via keys. */
    @NonNull
    private final Bundle mPropertyBundle;
    private final Map<String, Object> mProperties;

    /**
     * Create a new {@link AppSearchDocument}.
     * @param proto Contains {@link AppSearchDocument} basic information (uri, schemaType etc) and
     *               properties ordered by keys.
     * @param propertyBundle Contains all properties in {@link #mProto} to support get
     *                        properties via keys.
     * @param propertiesMap Contains all properties in {@link #mProto} to support get properties
     *                      via keys.
     */
    private AppSearchDocument(@NonNull DocumentProto proto, @NonNull Bundle propertyBundle) {
        this.mProto = proto;
        this.mPropertyBundle = propertyBundle;
    private AppSearchDocument(@NonNull DocumentProto proto,
            @NonNull Map<String, Object> propertiesMap) {
        mProto = proto;
        mProperties = propertiesMap;
    }

    /**
@@ -88,12 +89,12 @@ public class AppSearchDocument {
     * <p>This method should be only used by constructor of a subclass.
     */
    protected AppSearchDocument(@NonNull AppSearchDocument document) {
        this(document.mProto, document.mPropertyBundle);
        this(document.mProto, document.mProperties);
    }

    /** @hide */
    AppSearchDocument(@NonNull DocumentProto documentProto) {
        this(documentProto, new Bundle());
        this(documentProto, new ArrayMap<>());
        for (int i = 0; i < documentProto.getPropertiesCount(); i++) {
            PropertyProto property = documentProto.getProperties(i);
            String name = property.getName();
@@ -102,38 +103,38 @@ public class AppSearchDocument {
                for (int j = 0; j < values.length; j++) {
                    values[j] = property.getStringValues(j);
                }
                mPropertyBundle.putStringArray(name, values);
                mProperties.put(name, values);
            } else if (property.getInt64ValuesCount() > 0) {
                long[] values = new long[property.getInt64ValuesCount()];
                for (int j = 0; j < values.length; j++) {
                    values[j] = property.getInt64Values(j);
                }
                mPropertyBundle.putLongArray(property.getName(), values);
                mProperties.put(property.getName(), values);
            } else if (property.getDoubleValuesCount() > 0) {
                double[] values = new double[property.getDoubleValuesCount()];
                for (int j = 0; j < values.length; j++) {
                    values[j] = property.getDoubleValues(j);
                }
                mPropertyBundle.putDoubleArray(property.getName(), values);
                mProperties.put(property.getName(), values);
            } else if (property.getBooleanValuesCount() > 0) {
                boolean[] values = new boolean[property.getBooleanValuesCount()];
                for (int j = 0; j < values.length; j++) {
                    values[j] = property.getBooleanValues(j);
                }
                mPropertyBundle.putBooleanArray(property.getName(), values);
                mProperties.put(property.getName(), values);
            } else if (property.getBytesValuesCount() > 0) {
                byte[][] values = new byte[property.getBytesValuesCount()][];
                for (int j = 0; j < values.length; j++) {
                    values[j] = property.getBytesValues(j).toByteArray();
                }
                mPropertyBundle.putObject(name, values);
                mProperties.put(name, values);
            } else if (property.getDocumentValuesCount() > 0) {
                AppSearchDocument[] values =
                        new AppSearchDocument[property.getDocumentValuesCount()];
                for (int j = 0; j < values.length; j++) {
                    values[j] = new AppSearchDocument(property.getDocumentValues(j));
                }
                mPropertyBundle.putObject(name, values);
                mProperties.put(name, values);
            } else {
                throw new IllegalStateException("Unknown type of value: " + name);
            }
@@ -183,6 +184,19 @@ public class AppSearchDocument {
        return mProto.getCreationTimestampMs();
    }

    /**
     * Returns the TTL (Time To Live) of the {@link AppSearchDocument}, in milliseconds.
     *
     * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
     *    until the app is uninstalled.
     *
     * @hide
     */
    @DurationMillisLong
    public long getTtlMillis() {
        return mProto.getTtlMs();
    }

    /**
     * Returns the score of the {@link AppSearchDocument}.
     *
@@ -270,6 +284,23 @@ public class AppSearchDocument {
        return propertyArray[0];
    }

    /**
     * Retrieve a {@code byte[]} value by key.
     *
     * @param key The key to look for.
     * @return The first {@code byte[]} associated with the given key or {@code null} if there
     *         is no such key or the value is of a different type.
     */
    @Nullable
    public byte[] getPropertyBytes(@NonNull String key) {
        byte[][] propertyArray = getPropertyBytesArray(key);
        if (ArrayUtils.isEmpty(propertyArray)) {
            return null;
        }
        warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length);
        return propertyArray[0];
    }

    /** Prints a warning to logcat if the given propertyLength is greater than 1. */
    private static void warnIfSinglePropertyTooLong(
            @NonNull String propertyType, @NonNull String key, int propertyLength) {
@@ -333,13 +364,25 @@ public class AppSearchDocument {
        return getAndCastPropertyArray(key, boolean[].class);
    }

    /**
     * Retrieve a {@code byte[][]} property by key.
     *
     * @param key The key to look for.
     * @return The {@code byte[][]} associated with the given key, or {@code null} if no value
     *         is set or the value is of a different type.
     */
    @Nullable
    public byte[][] getPropertyBytesArray(@NonNull String key) {
        return getAndCastPropertyArray(key, byte[][].class);
    }

    /**
     * Gets a repeated property of the given key, and casts it to the given class type, which
     * must be an array class type.
     */
    @Nullable
    private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
        Object value = mPropertyBundle.get(key);
        Object value = mProperties.get(key);
        if (value == null) {
            return null;
        }
@@ -354,7 +397,7 @@ public class AppSearchDocument {
    @Override
    public boolean equals(@Nullable Object other) {
        // Check only proto's equality is sufficient here since all properties in
        // mPropertyBundle are ordered by keys and stored in proto.
        // mProperties are ordered by keys and stored in proto.
        if (this == other) {
            return true;
        }
@@ -367,8 +410,8 @@ public class AppSearchDocument {

    @Override
    public int hashCode() {
        // Hash only proto is sufficient here since all properties in mPropertyBundle are
        // ordered by keys and stored in proto.
        // Hash only proto is sufficient here since all properties in mProperties are ordered by
        // keys and stored in proto.
        return mProto.hashCode();
    }

@@ -385,7 +428,7 @@ public class AppSearchDocument {
     */
    public static class Builder<BuilderType extends Builder> {

        private final Bundle mPropertyBundle = new Bundle();
        private final Map<String, Object> mProperties = new ArrayMap<>();
        private final DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
        private final BuilderType mBuilderTypeInstance;

@@ -419,7 +462,7 @@ public class AppSearchDocument {
        @NonNull
        public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
            if (score < 0) {
                throw new IllegalArgumentException("Document score cannot be negative");
                throw new IllegalArgumentException("Document score cannot be negative.");
            }
            mProtoBuilder.setScore(score);
            return mBuilderTypeInstance;
@@ -438,6 +481,23 @@ public class AppSearchDocument {
            return mBuilderTypeInstance;
        }

        /**
         * Set the TTL (Time To Live) of the {@link AppSearchDocument}, in milliseconds.
         *
         * <p>After this many milliseconds since the {@link #setCreationTimestampMillis(long)}
         * creation timestamp}, the document is deleted.
         *
         * @param ttlMillis A non-negative duration in milliseconds.
         * @throws IllegalArgumentException If the provided value is negative.
         */
        @NonNull
        public BuilderType setTtlMillis(@DurationMillisLong long ttlMillis) {
            Preconditions.checkArgumentNonNegative(
                    ttlMillis, "Document ttlMillis cannot be negative.");
            mProtoBuilder.setTtlMs(ttlMillis);
            return mBuilderTypeInstance;
        }

        /**
         * Sets one or multiple {@code String} values for a property, replacing its previous
         * values.
@@ -448,7 +508,7 @@ public class AppSearchDocument {
         */
        @NonNull
        public BuilderType setProperty(@NonNull String key, @NonNull String... values) {
            putInBundle(mPropertyBundle, key, values);
            putInPropertyMap(key, values);
            return mBuilderTypeInstance;
        }

@@ -457,12 +517,11 @@ public class AppSearchDocument {
         * values.
         *
         * @param key The key associated with the {@code values}.
         * @param values The {@code boolean} values of the schema.org property.
         * @hide
         * @param values The {@code boolean} values of the property.
         */
        @NonNull
        public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) {
            putInBundle(mPropertyBundle, key, values);
            putInPropertyMap(key, values);
            return mBuilderTypeInstance;
        }

@@ -471,12 +530,11 @@ public class AppSearchDocument {
         * values.
         *
         * @param key The key associated with the {@code values}.
         * @param values The {@code long} values of the schema.org property.
         * @hide
         * @param values The {@code long} values of the property.
         */
        @NonNull
        public BuilderType setProperty(@NonNull String key, @NonNull long... values) {
            putInBundle(mPropertyBundle, key, values);
            putInPropertyMap(key, values);
            return mBuilderTypeInstance;
        }

@@ -485,17 +543,27 @@ public class AppSearchDocument {
         * values.
         *
         * @param key The key associated with the {@code values}.
         * @param values The {@code double} values of the schema.org property.
         * @hide
         * @param values The {@code double} values of the property.
         */
        @NonNull
        public BuilderType setProperty(@NonNull String key, @NonNull double... values) {
            putInBundle(mPropertyBundle, key, values);
            putInPropertyMap(key, values);
            return mBuilderTypeInstance;
        }

        private static void putInBundle(
                @NonNull Bundle bundle, @NonNull String key, @NonNull String... values)
        /**
         * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
         *
         * @param key The key associated with the {@code values}.
         * @param values The {@code byte[]} of the property.
         */
        @NonNull
        public BuilderType setProperty(@NonNull String key, @NonNull byte[]... values) {
            putInPropertyMap(key, values);
            return mBuilderTypeInstance;
        }

        private void putInPropertyMap(@NonNull String key, @NonNull String[] values)
                throws IllegalArgumentException {
            Objects.requireNonNull(key);
            Objects.requireNonNull(values);
@@ -509,33 +577,38 @@ public class AppSearchDocument {
                            + MAX_STRING_LENGTH + ".");
                }
            }
            bundle.putStringArray(key, values);
            mProperties.put(key, values);
        }

        private static void putInBundle(
                @NonNull Bundle bundle, @NonNull String key, @NonNull boolean... values) {
        private void putInPropertyMap(@NonNull String key, @NonNull boolean[] values) {
            Objects.requireNonNull(key);
            Objects.requireNonNull(values);
            validateRepeatedPropertyLength(key, values.length);
            bundle.putBooleanArray(key, values);
            mProperties.put(key, values);
        }

        private static void putInBundle(
                @NonNull Bundle bundle, @NonNull String key, @NonNull double... values) {
        private void putInPropertyMap(@NonNull String key, @NonNull double[] values) {
            Objects.requireNonNull(key);
            Objects.requireNonNull(values);
            validateRepeatedPropertyLength(key, values.length);
            bundle.putDoubleArray(key, values);
            mProperties.put(key, values);
        }

        private static void putInBundle(
                @NonNull Bundle bundle, @NonNull String key, @NonNull long... values) {
        private void putInPropertyMap(@NonNull String key, @NonNull long[] values) {
            Objects.requireNonNull(key);
            Objects.requireNonNull(values);
            validateRepeatedPropertyLength(key, values.length);
            bundle.putLongArray(key, values);
            mProperties.put(key, values);
        }

        private void putInPropertyMap(@NonNull String key, @NonNull byte[][] values) {
            Objects.requireNonNull(key);
            Objects.requireNonNull(values);
            validateRepeatedPropertyLength(key, values.length);
            mProperties.put(key, values);
        }


        private static void validateRepeatedPropertyLength(@NonNull String key, int length) {
            if (length == 0) {
                throw new IllegalArgumentException("The input array is empty.");
@@ -552,14 +625,15 @@ public class AppSearchDocument {
         * @hide
         */
        public AppSearchDocument build() {
            // Build proto by sorting the keys in propertyBundle to exclude the influence of
            // Build proto by sorting the keys in mProperties to exclude the influence of
            // order. Therefore documents will generate same proto as long as the contents are
            // same. Note that the order of repeated fields is still preserved.
            ArrayList<String> keys = new ArrayList<>(mPropertyBundle.keySet());
            ArrayList<String> keys = new ArrayList<>(mProperties.keySet());
            Collections.sort(keys);
            for (String key : keys) {
                Object values = mPropertyBundle.get(key);
                PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(key);
            for (int i = 0; i < keys.size(); i++) {
                String name = keys.get(i);
                Object values = mProperties.get(name);
                PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(name);
                if (values instanceof boolean[]) {
                    for (boolean value : (boolean[]) values) {
                        propertyProto.addBooleanValues(value);
@@ -576,14 +650,18 @@ public class AppSearchDocument {
                    for (String value : (String[]) values) {
                        propertyProto.addStringValues(value);
                    }
                } else if (values instanceof byte[][]) {
                    for (byte[] value : (byte[][]) values) {
                        propertyProto.addBytesValues(ByteString.copyFrom(value));
                    }
                } else {
                    throw new IllegalStateException(
                            "Property \"" + key + "\" has unsupported value type \""
                            "Property \"" + name + "\" has unsupported value type \""
                                    + values.getClass().getSimpleName() + "\"");
                }
                mProtoBuilder.addProperties(propertyProto);
            }
            return new AppSearchDocument(mProtoBuilder.build(), mPropertyBundle);
            return new AppSearchDocument(mProtoBuilder.build(), mProperties);
        }
    }
}
+28 −2
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import androidx.test.filters.SmallTest;

import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.PropertyProto;
import com.google.android.icing.protobuf.ByteString;

import org.junit.Test;

@@ -34,22 +35,28 @@ import java.util.List;

@SmallTest
public class AppSearchDocumentTest {
    private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
    private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};

    @Test
    public void testDocumentEquals_Identical() {
        AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
                .setCreationTimestampMillis(5L)
                .setTtlMillis(1L)
                .setProperty("longKey1", 1L, 2L, 3L)
                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
                .setProperty("booleanKey1", true, false, true)
                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
                .setProperty("byteKey1", sByteArray1, sByteArray2)
                .build();
        AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
                .setCreationTimestampMillis(5L)
                .setTtlMillis(1L)
                .setProperty("longKey1", 1L, 2L, 3L)
                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
                .setProperty("booleanKey1", true, false, true)
                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
                .setProperty("byteKey1", sByteArray1, sByteArray2)
                .build();
        assertThat(document1).isEqualTo(document2);
        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
@@ -60,6 +67,7 @@ public class AppSearchDocumentTest {
        AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
                .setCreationTimestampMillis(5L)
                .setProperty("longKey1", 1L, 2L, 3L)
                .setProperty("byteKey1", sByteArray1, sByteArray2)
                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
                .setProperty("booleanKey1", true, false, true)
                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
@@ -71,6 +79,7 @@ public class AppSearchDocumentTest {
                .setProperty("booleanKey1", true, false, true)
                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
                .setProperty("byteKey1", sByteArray1, sByteArray2)
                .setProperty("longKey1", 1L, 2L, 3L)
                .build();
        assertThat(document1).isEqualTo(document2);
@@ -114,11 +123,14 @@ public class AppSearchDocumentTest {
        AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
                .setCreationTimestampMillis(5L)
                .setScore(1)
                .setTtlMillis(1L)
                .setProperty("longKey1", 1L)
                .setProperty("doubleKey1", 1.0)
                .setProperty("booleanKey1", true)
                .setProperty("stringKey1", "test-value1").build();
                .setProperty("stringKey1", "test-value1")
                .setProperty("byteKey1", sByteArray1).build();
        assertThat(document.getUri()).isEqualTo("uri1");
        assertThat(document.getTtlMillis()).isEqualTo(1L);
        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
        assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
        assertThat(document.getScore()).isEqualTo(1);
@@ -126,6 +138,8 @@ public class AppSearchDocumentTest {
        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
        assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
        assertThat(document.getPropertyBytes("byteKey1"))
                .asList().containsExactly((byte) 1, (byte) 2, (byte) 3);
    }

    @Test
@@ -136,6 +150,7 @@ public class AppSearchDocumentTest {
                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
                .setProperty("booleanKey1", true, false, true)
                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
                .setProperty("byteKey1", sByteArray1, sByteArray2)
                .build();

        assertThat(document.getUri()).isEqualTo("uri1");
@@ -147,6 +162,8 @@ public class AppSearchDocumentTest {
                .containsExactly(true, false, true);
        assertThat(document.getPropertyStringArray("stringKey1")).asList()
                .containsExactly("test-value1", "test-value2", "test-value3");
        assertThat(document.getPropertyBytesArray("byteKey1")).asList()
                .containsExactly(sByteArray1, sByteArray2);
    }

    @Test
@@ -188,15 +205,21 @@ public class AppSearchDocumentTest {
        AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
                .setCreationTimestampMillis(5L)
                .setScore(1)
                .setTtlMillis(1L)
                .setProperty("longKey1", 1L)
                .setProperty("doubleKey1", 1.0)
                .setProperty("booleanKey1", true)
                .setProperty("stringKey1", "test-value1")
                .setProperty("byteKey1", sByteArray1)
                .build();

        // Create the Document proto. Need to sort the property order by key.
        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
                .setUri("uri1").setSchema("schemaType1").setScore(1).setCreationTimestampMs(5L);
                .setUri("uri1")
                .setSchema("schemaType1")
                .setCreationTimestampMs(5L)
                .setScore(1)
                .setTtlMs(1L);
        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
        propertyProtoMap.put("longKey1",
                PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
@@ -206,6 +229,9 @@ public class AppSearchDocumentTest {
                PropertyProto.newBuilder().setName("booleanKey1").addBooleanValues(true));
        propertyProtoMap.put("stringKey1",
                PropertyProto.newBuilder().setName("stringKey1").addStringValues("test-value1"));
        propertyProtoMap.put("byteKey1",
                PropertyProto.newBuilder().setName("byteKey1").addBytesValues(
                        ByteString.copyFrom(sByteArray1)));
        List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
        Collections.sort(sortedKey);
        for (String key : sortedKey) {
+7 −0
Original line number Diff line number Diff line
@@ -34,6 +34,10 @@ import org.junit.Test;
 */
@SmallTest
public class CustomerDocumentTest {

    private static byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
    private static byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};

    @Test
    public void testBuildCustomerDocument() {
        CustomerDocument customerDocument = new CustomerDocument.Builder("uri1")
@@ -43,6 +47,7 @@ public class CustomerDocumentTest {
                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
                .setProperty("booleanKey1", true, false, true)
                .setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
                .setProperty("byteKey1", sByteArray1, sByteArray2)
                .build();

        assertThat(customerDocument.getUri()).isEqualTo("uri1");
@@ -57,6 +62,8 @@ public class CustomerDocumentTest {
                .containsExactly(true, false, true);
        assertThat(customerDocument.getPropertyStringArray("stringKey1")).asList()
                .containsExactly("test-value1", "test-value2", "test-value3");
        assertThat(customerDocument.getPropertyBytesArray("byteKey1")).asList()
                .containsExactly(sByteArray1, sByteArray2);
    }

    /**