diff options
| author | 2020-10-17 01:11:05 +0000 | |
|---|---|---|
| committer | 2020-10-17 01:11:05 +0000 | |
| commit | 286e15457f4adba4dee773e701e88c121c133056 (patch) | |
| tree | f68b03a0221654b7664fe292fd27f59ada67384d | |
| parent | 4688072c5410bfc4444c2ef25c87fff906696884 (diff) | |
| parent | bd6bcf6a2a4b5b1767936c454b580b85661c00ed (diff) | |
Merge "Merge GenericDocument and SearchResult work from Jetpack."
19 files changed, 1926 insertions, 1288 deletions
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java deleted file mode 100644 index 7d2b64e5d882..000000000000 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java +++ /dev/null @@ -1,698 +0,0 @@ -/* - * Copyright (C) 2020 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; - -import android.annotation.CurrentTimeMillisLong; -import android.annotation.DurationMillisLong; -import android.annotation.IntRange; -import android.annotation.NonNull; -import android.annotation.Nullable; -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.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 - */ -public class AppSearchDocument { - private static final String TAG = "AppSearchDocument"; - - /** The default empty namespace.*/ - // TODO(adorokhine): Allow namespace to be specified in the document. - public static final String DEFAULT_NAMESPACE = ""; - - /** - * The maximum number of elements in a repeatable field. Will reject the request if exceed - * this limit. - */ - private static final int MAX_REPEATED_PROPERTY_LENGTH = 100; - - /** - * The maximum {@link String#length} of a {@link String} field. Will reject the request if - * {@link String}s longer than this. - */ - private static final int MAX_STRING_LENGTH = 20_000; - - /** - * Contains {@link AppSearchDocument} basic information (uri, schemaType etc) and properties - * ordered by keys. - */ - @NonNull - private final DocumentProto mProto; - - /** Contains all properties in {@link #mProto} to support getting properties via keys. */ - @NonNull - private final Map<String, Object> mProperties; - - /** - * Creates a new {@link AppSearchDocument}. - * @param proto Contains {@link AppSearchDocument} basic information (uri, schemaType etc) and - * properties ordered by keys. - * @param propertiesMap Contains all properties in {@link #mProto} to support get properties - * via keys. - */ - private AppSearchDocument(@NonNull DocumentProto proto, - @NonNull Map<String, Object> propertiesMap) { - mProto = proto; - mProperties = propertiesMap; - } - - /** - * Creates a new {@link AppSearchDocument} from an existing instance. - * - * <p>This method should be only used by constructor of a subclass. - */ - protected AppSearchDocument(@NonNull AppSearchDocument document) { - this(document.mProto, document.mProperties); - } - - /** @hide */ - AppSearchDocument(@NonNull DocumentProto documentProto) { - this(documentProto, new ArrayMap<>()); - for (int i = 0; i < documentProto.getPropertiesCount(); i++) { - PropertyProto property = documentProto.getProperties(i); - String name = property.getName(); - if (property.getStringValuesCount() > 0) { - String[] values = new String[property.getStringValuesCount()]; - for (int j = 0; j < values.length; j++) { - values[j] = property.getStringValues(j); - } - 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); - } - 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); - } - 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); - } - 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(); - } - 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)); - } - mProperties.put(name, values); - } else { - throw new IllegalStateException("Unknown type of value: " + name); - } - } - } - - /** - * Returns the {@link DocumentProto} of the {@link AppSearchDocument}. - * - * <p>The {@link DocumentProto} contains {@link AppSearchDocument}'s basic information and all - * properties ordered by keys. - * @hide - */ - @NonNull - @VisibleForTesting - public DocumentProto getProto() { - return mProto; - } - - /** Returns the URI of the {@link AppSearchDocument}. */ - @NonNull - public String getUri() { - return mProto.getUri(); - } - - /** Returns the schema type of the {@link AppSearchDocument}. */ - @NonNull - public String getSchemaType() { - return mProto.getSchema(); - } - - /** - * Returns the creation timestamp in milliseconds of the {@link AppSearchDocument}. Value will - * be in the {@link System#currentTimeMillis()} time base. - */ - @CurrentTimeMillisLong - public long getCreationTimestampMillis() { - 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. - */ - @DurationMillisLong - public long getTtlMillis() { - return mProto.getTtlMs(); - } - - /** - * Returns the score of the {@link AppSearchDocument}. - * - * <p>The score is a query-independent measure of the document's quality, relative to other - * {@link AppSearchDocument}s of the same type. - * - * <p>The default value is 0. - */ - public int getScore() { - return mProto.getScore(); - } - - /** - * Retrieve a {@link String} value by key. - * - * @param key The key to look for. - * @return The first {@link String} associated with the given key or {@code null} if there - * is no such key or the value is of a different type. - */ - @Nullable - public String getPropertyString(@NonNull String key) { - String[] propertyArray = getPropertyStringArray(key); - if (ArrayUtils.isEmpty(propertyArray)) { - return null; - } - warnIfSinglePropertyTooLong("String", key, propertyArray.length); - return propertyArray[0]; - } - - /** - * Retrieve a {@code long} value by key. - * - * @param key The key to look for. - * @return The first {@code long} associated with the given key or default value {@code 0} if - * there is no such key or the value is of a different type. - */ - public long getPropertyLong(@NonNull String key) { - long[] propertyArray = getPropertyLongArray(key); - if (ArrayUtils.isEmpty(propertyArray)) { - return 0; - } - warnIfSinglePropertyTooLong("Long", key, propertyArray.length); - return propertyArray[0]; - } - - /** - * Retrieve a {@code double} value by key. - * - * @param key The key to look for. - * @return The first {@code double} associated with the given key or default value {@code 0.0} - * if there is no such key or the value is of a different type. - */ - public double getPropertyDouble(@NonNull String key) { - double[] propertyArray = getPropertyDoubleArray(key); - // TODO(tytytyww): Add support double array to ArraysUtils.isEmpty(). - if (propertyArray == null || propertyArray.length == 0) { - return 0.0; - } - warnIfSinglePropertyTooLong("Double", key, propertyArray.length); - return propertyArray[0]; - } - - /** - * Retrieve a {@code boolean} value by key. - * - * @param key The key to look for. - * @return The first {@code boolean} associated with the given key or default value - * {@code false} if there is no such key or the value is of a different type. - */ - public boolean getPropertyBoolean(@NonNull String key) { - boolean[] propertyArray = getPropertyBooleanArray(key); - if (ArrayUtils.isEmpty(propertyArray)) { - return false; - } - warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length); - 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]; - } - - /** - * Retrieve a {@link AppSearchDocument} value by key. - * - * @param key The key to look for. - * @return The first {@link AppSearchDocument} associated with the given key or {@code null} if - * there is no such key or the value is of a different type. - */ - @Nullable - public AppSearchDocument getPropertyDocument(@NonNull String key) { - AppSearchDocument[] propertyArray = getPropertyDocumentArray(key); - if (ArrayUtils.isEmpty(propertyArray)) { - return null; - } - warnIfSinglePropertyTooLong("Document", 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) { - if (propertyLength > 1) { - Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength - + " elements. Only the first one will be returned from " - + "getProperty" + propertyType + "(). Try getProperty" + propertyType - + "Array()."); - } - } - - /** - * Retrieve a repeated {@link String} property by key. - * - * @param key The key to look for. - * @return The {@code String[]} associated with the given key, or {@code null} if no value - * is set or the value is of a different type. - */ - @Nullable - public String[] getPropertyStringArray(@NonNull String key) { - return getAndCastPropertyArray(key, String[].class); - } - - /** - * Retrieve a repeated {@code long} property by key. - * - * @param key The key to look for. - * @return The {@code long[]} associated with the given key, or {@code null} if no value is - * set or the value is of a different type. - */ - @Nullable - public long[] getPropertyLongArray(@NonNull String key) { - return getAndCastPropertyArray(key, long[].class); - } - - /** - * Retrieve a repeated {@code double} property by key. - * - * @param key The key to look for. - * @return The {@code double[]} associated with the given key, or {@code null} if no value - * is set or the value is of a different type. - */ - @Nullable - public double[] getPropertyDoubleArray(@NonNull String key) { - return getAndCastPropertyArray(key, double[].class); - } - - /** - * Retrieve a repeated {@code boolean} property by key. - * - * @param key The key to look for. - * @return The {@code boolean[]} associated with the given key, or {@code null} if no value - * is set or the value is of a different type. - */ - @Nullable - public boolean[] getPropertyBooleanArray(@NonNull String key) { - 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); - } - - /** - * Retrieve a repeated {@link AppSearchDocument} property by key. - * - * @param key The key to look for. - * @return The {@link AppSearchDocument[]} associated with the given key, or {@code null} if no - * value is set or the value is of a different type. - */ - @Nullable - public AppSearchDocument[] getPropertyDocumentArray(@NonNull String key) { - return getAndCastPropertyArray(key, AppSearchDocument[].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 = mProperties.get(key); - if (value == null) { - return null; - } - try { - return tClass.cast(value); - } catch (ClassCastException e) { - Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e); - return null; - } - } - - @Override - public boolean equals(@Nullable Object other) { - // Check only proto's equality is sufficient here since all properties in - // mProperties are ordered by keys and stored in proto. - if (this == other) { - return true; - } - if (!(other instanceof AppSearchDocument)) { - return false; - } - AppSearchDocument otherDocument = (AppSearchDocument) other; - return this.mProto.equals(otherDocument.mProto); - } - - @Override - public int hashCode() { - // Hash only proto is sufficient here since all properties in mProperties are ordered by - // keys and stored in proto. - return mProto.hashCode(); - } - - @Override - public String toString() { - return mProto.toString(); - } - - /** - * The builder class for {@link AppSearchDocument}. - * - * @param <BuilderType> Type of subclass who extend this. - */ - public static class Builder<BuilderType extends Builder> { - - private final Map<String, Object> mProperties = new ArrayMap<>(); - private final DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder(); - private final BuilderType mBuilderTypeInstance; - - /** - * Creates a new {@link AppSearchDocument.Builder}. - * - * <p>The URI is a unique string opaque to AppSearch. - * - * @param uri The uri of {@link AppSearchDocument}. - * @param schemaType The schema type of the {@link AppSearchDocument}. The passed-in - * {@code schemaType} must be defined using {@link AppSearchManager#setSchema} prior - * to inserting a document of this {@code schemaType} into the AppSearch index using - * {@link AppSearchManager#putDocuments(List)}. Otherwise, the document will be - * rejected by {@link AppSearchManager#putDocuments(List)}. - */ - public Builder(@NonNull String uri, @NonNull String schemaType) { - mBuilderTypeInstance = (BuilderType) this; - mProtoBuilder.setUri(uri).setSchema(schemaType).setNamespace(DEFAULT_NAMESPACE); - // Set current timestamp for creation timestamp by default. - setCreationTimestampMillis(System.currentTimeMillis()); - } - - /** - * Sets the score of the {@link AppSearchDocument}. - * - * <p>The score is a query-independent measure of the document's quality, relative to - * other {@link AppSearchDocument}s of the same type. - * - * @throws IllegalArgumentException If the provided value is negative. - */ - @NonNull - public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) { - if (score < 0) { - throw new IllegalArgumentException("Document score cannot be negative."); - } - mProtoBuilder.setScore(score); - return mBuilderTypeInstance; - } - - /** - * Set the creation timestamp in milliseconds of the {@link AppSearchDocument}. Should be - * set using a value obtained from the {@link System#currentTimeMillis()} time base. - */ - @NonNull - public BuilderType setCreationTimestampMillis( - @CurrentTimeMillisLong long creationTimestampMillis) { - mProtoBuilder.setCreationTimestampMs(creationTimestampMillis); - 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. - * - * @param key The key associated with the {@code values}. - * @param values The {@code String} values of the property. - */ - @NonNull - public BuilderType setProperty(@NonNull String key, @NonNull String... values) { - putInPropertyMap(key, values); - return mBuilderTypeInstance; - } - - /** - * Sets one or multiple {@code boolean} values for a property, replacing its previous - * values. - * - * @param key The key associated with the {@code values}. - * @param values The {@code boolean} values of the property. - */ - @NonNull - public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) { - putInPropertyMap(key, values); - return mBuilderTypeInstance; - } - - /** - * Sets one or multiple {@code long} values for a property, replacing its previous - * values. - * - * @param key The key associated with the {@code values}. - * @param values The {@code long} values of the property. - */ - @NonNull - public BuilderType setProperty(@NonNull String key, @NonNull long... values) { - putInPropertyMap(key, values); - return mBuilderTypeInstance; - } - - /** - * Sets one or multiple {@code double} values for a property, replacing its previous - * values. - * - * @param key The key associated with the {@code values}. - * @param values The {@code double} values of the property. - */ - @NonNull - public BuilderType setProperty(@NonNull String key, @NonNull double... values) { - putInPropertyMap(key, values); - return mBuilderTypeInstance; - } - - /** - * 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; - } - - /** - * Sets one or multiple {@link AppSearchDocument} values for a property, replacing its - * previous values. - * - * @param key The key associated with the {@code values}. - * @param values The {@link AppSearchDocument} values of the property. - */ - @NonNull - public BuilderType setProperty(@NonNull String key, @NonNull AppSearchDocument... values) { - putInPropertyMap(key, values); - return mBuilderTypeInstance; - } - - private void putInPropertyMap(@NonNull String key, @NonNull String[] values) - throws IllegalArgumentException { - Objects.requireNonNull(key); - Objects.requireNonNull(values); - validateRepeatedPropertyLength(key, values.length); - for (int i = 0; i < values.length; i++) { - if (values[i] == null) { - throw new IllegalArgumentException("The String at " + i + " is null."); - } else if (values[i].length() > MAX_STRING_LENGTH) { - throw new IllegalArgumentException("The String at " + i + " length is: " - + values[i].length() + ", which exceeds length limit: " - + MAX_STRING_LENGTH + "."); - } - } - mProperties.put(key, values); - } - - private void putInPropertyMap(@NonNull String key, @NonNull boolean[] values) { - Objects.requireNonNull(key); - Objects.requireNonNull(values); - validateRepeatedPropertyLength(key, values.length); - mProperties.put(key, values); - } - - private void putInPropertyMap(@NonNull String key, @NonNull double[] values) { - Objects.requireNonNull(key); - Objects.requireNonNull(values); - validateRepeatedPropertyLength(key, values.length); - mProperties.put(key, values); - } - - private void putInPropertyMap(@NonNull String key, @NonNull long[] values) { - Objects.requireNonNull(key); - Objects.requireNonNull(values); - validateRepeatedPropertyLength(key, values.length); - 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 void putInPropertyMap(@NonNull String key, @NonNull AppSearchDocument[] values) { - Objects.requireNonNull(key); - Objects.requireNonNull(values); - for (int i = 0; i < values.length; i++) { - if (values[i] == null) { - throw new IllegalArgumentException("The document at " + i + " is null."); - } - } - 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."); - } else if (length > MAX_REPEATED_PROPERTY_LENGTH) { - throw new IllegalArgumentException( - "Repeated property \"" + key + "\" has length " + length - + ", which exceeds the limit of " - + MAX_REPEATED_PROPERTY_LENGTH); - } - } - - /** Builds the {@link AppSearchDocument} object. */ - @NonNull - public AppSearchDocument build() { - // 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<>(mProperties.keySet()); - Collections.sort(keys); - 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); - } - } else if (values instanceof long[]) { - for (long value : (long[]) values) { - propertyProto.addInt64Values(value); - } - } else if (values instanceof double[]) { - for (double value : (double[]) values) { - propertyProto.addDoubleValues(value); - } - } else if (values instanceof String[]) { - for (String value : (String[]) values) { - propertyProto.addStringValues(value); - } - } else if (values instanceof AppSearchDocument[]) { - for (AppSearchDocument value : (AppSearchDocument[]) values) { - propertyProto.addDocumentValues(value.getProto()); - } - } else if (values instanceof byte[][]) { - for (byte[] value : (byte[][]) values) { - propertyProto.addBytesValues(ByteString.copyFrom(value)); - } - } else { - throw new IllegalStateException( - "Property \"" + name + "\" has unsupported value type \"" - + values.getClass().getSimpleName() + "\""); - } - mProtoBuilder.addProperties(propertyProto); - } - return new AppSearchDocument(mProtoBuilder.build(), mProperties); - } - } -} diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java index b13dd9f48c8d..5f2fabe52929 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2020 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. @@ -16,20 +16,26 @@ package android.app.appsearch; + import android.annotation.NonNull; import android.annotation.Nullable; + import android.app.appsearch.AppSearchSchema.PropertyConfig; /** - * Encapsulates a {@link AppSearchDocument} that represent an email. + * Encapsulates a {@link GenericDocument} that represent an email. * - * <p>This class is a higher level implement of {@link AppSearchDocument}. + * <p>This class is a higher level implement of {@link GenericDocument}. * * <p>This class will eventually migrate to Jetpack, where it will become public API. * * @hide */ -public class AppSearchEmail extends AppSearchDocument { + +public class AppSearchEmail extends GenericDocument { + /** The name of the schema type for {@link AppSearchEmail} documents.*/ + public static final String SCHEMA_TYPE = "builtin:Email"; + private static final String KEY_FROM = "from"; private static final String KEY_TO = "to"; private static final String KEY_CC = "cc"; @@ -37,46 +43,43 @@ public class AppSearchEmail extends AppSearchDocument { private static final String KEY_SUBJECT = "subject"; private static final String KEY_BODY = "body"; - /** The name of the schema type for {@link AppSearchEmail} documents.*/ - public static final String SCHEMA_TYPE = "builtin:Email"; - public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE) - .addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_FROM) + .addProperty(new PropertyConfig.Builder(KEY_FROM) .setDataType(PropertyConfig.DATA_TYPE_STRING) .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_TO) + ).addProperty(new PropertyConfig.Builder(KEY_TO) .setDataType(PropertyConfig.DATA_TYPE_STRING) .setCardinality(PropertyConfig.CARDINALITY_REPEATED) .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_CC) + ).addProperty(new PropertyConfig.Builder(KEY_CC) .setDataType(PropertyConfig.DATA_TYPE_STRING) .setCardinality(PropertyConfig.CARDINALITY_REPEATED) .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BCC) + ).addProperty(new PropertyConfig.Builder(KEY_BCC) .setDataType(PropertyConfig.DATA_TYPE_STRING) .setCardinality(PropertyConfig.CARDINALITY_REPEATED) .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_SUBJECT) + ).addProperty(new PropertyConfig.Builder(KEY_SUBJECT) .setDataType(PropertyConfig.DATA_TYPE_STRING) .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) .build() - ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BODY) + ).addProperty(new PropertyConfig.Builder(KEY_BODY) .setDataType(PropertyConfig.DATA_TYPE_STRING) .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) @@ -87,12 +90,11 @@ public class AppSearchEmail extends AppSearchDocument { /** * Creates a new {@link AppSearchEmail} from the contents of an existing - * {@link AppSearchDocument}. + * {@link GenericDocument}. * - * @param document The {@link AppSearchDocument} containing the email content. - * @hide + * @param document The {@link GenericDocument} containing the email content. */ - public AppSearchEmail(@NonNull AppSearchDocument document) { + public AppSearchEmail(@NonNull GenericDocument document) { super(document); } @@ -101,7 +103,6 @@ public class AppSearchEmail extends AppSearchDocument { * * @return Returns the subject of {@link AppSearchEmail} or {@code null} if it's not been set * yet. - * @hide */ @Nullable public String getFrom() { @@ -113,7 +114,6 @@ public class AppSearchEmail extends AppSearchDocument { * * @return Returns the destination addresses of {@link AppSearchEmail} or {@code null} if it's * not been set yet. - * @hide */ @Nullable public String[] getTo() { @@ -125,7 +125,6 @@ public class AppSearchEmail extends AppSearchDocument { * * @return Returns the CC list of {@link AppSearchEmail} or {@code null} if it's not been set * yet. - * @hide */ @Nullable public String[] getCc() { @@ -137,7 +136,6 @@ public class AppSearchEmail extends AppSearchDocument { * * @return Returns the BCC list of {@link AppSearchEmail} or {@code null} if it's not been set * yet. - * @hide */ @Nullable public String[] getBcc() { @@ -149,7 +147,6 @@ public class AppSearchEmail extends AppSearchDocument { * * @return Returns the value subject of {@link AppSearchEmail} or {@code null} if it's not been * set yet. - * @hide */ @Nullable public String getSubject() { @@ -160,7 +157,6 @@ public class AppSearchEmail extends AppSearchDocument { * Get the body of {@link AppSearchEmail}. * * @return Returns the body of {@link AppSearchEmail} or {@code null} if it's not been set yet. - * @hide */ @Nullable public String getBody() { @@ -169,14 +165,12 @@ public class AppSearchEmail extends AppSearchDocument { /** * The builder class for {@link AppSearchEmail}. - * @hide */ - public static class Builder extends AppSearchDocument.Builder<AppSearchEmail.Builder> { + public static class Builder extends GenericDocument.Builder<AppSearchEmail.Builder> { /** * Create a new {@link AppSearchEmail.Builder} * @param uri The Uri of the Email. - * @hide */ public Builder(@NonNull String uri) { super(uri, SCHEMA_TYPE); @@ -184,7 +178,6 @@ public class AppSearchEmail extends AppSearchDocument { /** * Set the from address of {@link AppSearchEmail} - * @hide */ @NonNull public AppSearchEmail.Builder setFrom(@NonNull String from) { @@ -194,7 +187,6 @@ public class AppSearchEmail extends AppSearchDocument { /** * Set the destination address of {@link AppSearchEmail} - * @hide */ @NonNull public AppSearchEmail.Builder setTo(@NonNull String... to) { @@ -204,7 +196,6 @@ public class AppSearchEmail extends AppSearchDocument { /** * Set the CC list of {@link AppSearchEmail} - * @hide */ @NonNull public AppSearchEmail.Builder setCc(@NonNull String... cc) { @@ -214,7 +205,6 @@ public class AppSearchEmail extends AppSearchDocument { /** * Set the BCC list of {@link AppSearchEmail} - * @hide */ @NonNull public AppSearchEmail.Builder setBcc(@NonNull String... bcc) { @@ -224,7 +214,6 @@ public class AppSearchEmail extends AppSearchDocument { /** * Set the subject of {@link AppSearchEmail} - * @hide */ @NonNull public AppSearchEmail.Builder setSubject(@NonNull String subject) { @@ -234,7 +223,6 @@ public class AppSearchEmail extends AppSearchDocument { /** * Set the body of {@link AppSearchEmail} - * @hide */ @NonNull public AppSearchEmail.Builder setBody(@NonNull String body) { @@ -242,11 +230,7 @@ public class AppSearchEmail extends AppSearchDocument { return this; } - /** - * Builds the {@link AppSearchEmail} object. - * - * @hide - */ + /** Builds the {@link AppSearchEmail} object. */ @NonNull @Override public AppSearchEmail build() { diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index ad51a5c4158e..b38bb0515e96 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -23,11 +23,6 @@ import android.os.RemoteException; import com.android.internal.infra.AndroidFuture; -import com.google.android.icing.proto.DocumentProto; -import com.google.android.icing.proto.SearchResultProto; -import com.google.android.icing.proto.StatusProto; -import com.google.protobuf.InvalidProtocolBufferException; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -78,8 +73,8 @@ public class AppSearchManager { * <li>Removal of an existing type * <li>Removal of a property from a type * <li>Changing the data type ({@code boolean}, {@code long}, etc.) of an existing property - * <li>For properties of {@code AppSearchDocument} type, changing the schema type of - * {@code AppSearchDocument}s of that property + * <li>For properties of {@code GenericDocument} type, changing the schema type of + * {@code GenericDocument}s of that property * <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an * {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL * OPTIONAL} property into a @@ -148,31 +143,31 @@ public class AppSearchManager { } /** - * Index {@link AppSearchDocument}s into AppSearch. + * Index {@link GenericDocument}s into AppSearch. * * <p>You should not call this method directly; instead, use the * {@code AppSearch#putDocuments()} API provided by JetPack. * - * <p>Each {@link AppSearchDocument}'s {@code schemaType} field must be set to the name of a + * <p>Each {@link GenericDocument}'s {@code schemaType} field must be set to the name of a * schema type previously registered via the {@link #setSchema} method. * - * @param documents {@link AppSearchDocument}s that need to be indexed. + * @param documents {@link GenericDocument}s that need to be indexed. * @return An {@link AppSearchBatchResult} mapping the document URIs to {@link Void} if they * were successfully indexed, or a {@link Throwable} describing the failure if they could * not be indexed. * @hide */ public AppSearchBatchResult<String, Void> putDocuments( - @NonNull List<AppSearchDocument> documents) { + @NonNull List<GenericDocument> documents) { // TODO(b/146386470): Transmit these documents as a RemoteStream instead of sending them in // one big list. - List<byte[]> documentsBytes = new ArrayList<>(documents.size()); - for (AppSearchDocument document : documents) { - documentsBytes.add(document.getProto().toByteArray()); + List<Bundle> documentBundles = new ArrayList<>(documents.size()); + for (GenericDocument document : documents) { + documentBundles.add(document.getBundle()); } AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>(); try { - mService.putDocuments(documentsBytes, future); + mService.putDocuments(documentBundles, future); } catch (RemoteException e) { future.completeExceptionally(e); } @@ -180,19 +175,18 @@ public class AppSearchManager { } /** - * Retrieves {@link AppSearchDocument}s by URI. + * Retrieves {@link GenericDocument}s by URI. * * <p>You should not call this method directly; instead, use the * {@code AppSearch#getDocuments()} API provided by JetPack. * * @param uris URIs of the documents to look up. * @return An {@link AppSearchBatchResult} mapping the document URIs to - * {@link AppSearchDocument} values if they were successfully retrieved, a {@code null} + * {@link GenericDocument} values if they were successfully retrieved, a {@code null} * failure if they were not found, or a {@link Throwable} failure describing the problem if * an error occurred. */ - public AppSearchBatchResult<String, AppSearchDocument> getDocuments( - @NonNull List<String> uris) { + public AppSearchBatchResult<String, GenericDocument> getDocuments(@NonNull List<String> uris) { // TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending // them in one big list. AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>(); @@ -202,43 +196,35 @@ public class AppSearchManager { future.completeExceptionally(e); } - // Deserialize the protos into Document objects - AppSearchBatchResult<String, byte[]> protoResults = getFutureOrThrow(future); - AppSearchBatchResult.Builder<String, AppSearchDocument> documentResultBuilder = + // Translate from document bundles to GenericDocument instances + AppSearchBatchResult<String, Bundle> bundleResult = getFutureOrThrow(future); + AppSearchBatchResult.Builder<String, GenericDocument> documentResultBuilder = new AppSearchBatchResult.Builder<>(); // Translate successful results - for (Map.Entry<String, byte[]> protoResult : protoResults.getSuccesses().entrySet()) { - DocumentProto documentProto; - try { - documentProto = DocumentProto.parseFrom(protoResult.getValue()); - } catch (InvalidProtocolBufferException e) { - documentResultBuilder.setFailure( - protoResult.getKey(), AppSearchResult.RESULT_IO_ERROR, e.getMessage()); - continue; - } - AppSearchDocument document; + for (Map.Entry<String, Bundle> bundleEntry : bundleResult.getSuccesses().entrySet()) { + GenericDocument document; try { - document = new AppSearchDocument(documentProto); + document = new GenericDocument(bundleEntry.getValue()); } catch (Throwable t) { // These documents went through validation, so how could this fail? We must have // done something wrong. documentResultBuilder.setFailure( - protoResult.getKey(), + bundleEntry.getKey(), AppSearchResult.RESULT_INTERNAL_ERROR, t.getMessage()); continue; } - documentResultBuilder.setSuccess(protoResult.getKey(), document); + documentResultBuilder.setSuccess(bundleEntry.getKey(), document); } // Translate failed results - for (Map.Entry<String, AppSearchResult<byte[]>> protoResult : - protoResults.getFailures().entrySet()) { + for (Map.Entry<String, AppSearchResult<Bundle>> bundleEntry : + bundleResult.getFailures().entrySet()) { documentResultBuilder.setFailure( - protoResult.getKey(), - protoResult.getValue().getResultCode(), - protoResult.getValue().getErrorMessage()); + bundleEntry.getKey(), + bundleEntry.getValue().getResultCode(), + bundleEntry.getValue().getErrorMessage()); } return documentResultBuilder.build(); @@ -287,43 +273,29 @@ public class AppSearchManager { * @hide */ @NonNull - public AppSearchResult<SearchResults> query( + public AppSearchResult<List<SearchResult>> query( @NonNull String queryExpression, @NonNull SearchSpec searchSpec) { // TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending // them in one big list. - AndroidFuture<AppSearchResult> searchResultFuture = new AndroidFuture<>(); + AndroidFuture<AppSearchResult> searchResultsFuture = new AndroidFuture<>(); try { - mService.query(queryExpression, searchSpec.getBundle(), searchResultFuture); + mService.query(queryExpression, searchSpec.getBundle(), searchResultsFuture); } catch (RemoteException e) { - searchResultFuture.completeExceptionally(e); + searchResultsFuture.completeExceptionally(e); } - // Deserialize the protos into Document objects - AppSearchResult<byte[]> searchResultBytes = getFutureOrThrow(searchResultFuture); - if (!searchResultBytes.isSuccess()) { + // Translate the list of Bundle into a list of SearchResult + AppSearchResult<SearchResults> searchResultsResult = getFutureOrThrow(searchResultsFuture); + if (!searchResultsResult.isSuccess()) { return AppSearchResult.newFailedResult( - searchResultBytes.getResultCode(), searchResultBytes.getErrorMessage()); + searchResultsResult.getResultCode(), searchResultsResult.getErrorMessage()); } - SearchResultProto searchResultProto; - try { - searchResultProto = SearchResultProto.parseFrom(searchResultBytes.getResultValue()); - } catch (InvalidProtocolBufferException e) { - return AppSearchResult.newFailedResult( - AppSearchResult.RESULT_INTERNAL_ERROR, e.getMessage()); - } - if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) { - // This should never happen; AppSearchManagerService should catch failed searchResults - // entries and transmit them as a failed AppSearchResult. - return AppSearchResult.newFailedResult( - AppSearchResult.RESULT_INTERNAL_ERROR, - searchResultProto.getStatus().getMessage()); - } - - return AppSearchResult.newSuccessfulResult(new SearchResults(searchResultProto)); + SearchResults searchResults = searchResultsResult.getResultValue(); + return AppSearchResult.newSuccessfulResult(searchResults.mResults); } /** - * Deletes {@link AppSearchDocument}s by URI. + * Deletes {@link GenericDocument}s by URI. * * <p>You should not call this method directly; instead, use the {@code AppSearch#delete()} API * provided by JetPack. diff --git a/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java b/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java new file mode 100644 index 000000000000..9fe2c67d00f2 --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java @@ -0,0 +1,923 @@ +/* + * Copyright 2020 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; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.util.Log; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import android.app.appsearch.exceptions.AppSearchException; +import com.android.internal.util.Preconditions; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Set; + +/** + * Represents a document unit. + * + * <p>Documents are constructed via {@link GenericDocument.Builder}. + * @hide + */ +public class GenericDocument { + private static final String TAG = "GenericDocument"; + + /** The default empty namespace.*/ + public static final String DEFAULT_NAMESPACE = ""; + + /** + * The maximum number of elements in a repeatable field. Will reject the request if exceed + * this limit. + */ + private static final int MAX_REPEATED_PROPERTY_LENGTH = 100; + + /** + * The maximum {@link String#length} of a {@link String} field. Will reject the request if + * {@link String}s longer than this. + */ + private static final int MAX_STRING_LENGTH = 20_000; + + /** The maximum number of indexed properties a document can have. */ + private static final int MAX_INDEXED_PROPERTIES = 16; + + /** 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; + + /** @hide */ + + public static final String PROPERTIES_FIELD = "properties"; + + /** @hide */ + + public static final String BYTE_ARRAY_FIELD = "byteArray"; + + static final String SCHEMA_TYPE_FIELD = "schemaType"; + static final String URI_FIELD = "uri"; + static final String SCORE_FIELD = "score"; + static final String TTL_MILLIS_FIELD = "ttlMillis"; + static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis"; + static final String NAMESPACE_FIELD = "namespace"; + + /** + * The maximum number of indexed properties a document can have. + * + * <p>Indexed properties are properties where the + * {@link android.app.appsearch.annotation.AppSearchDocument.Property#indexingType} constant is + * anything other than {@link + * android.app.appsearch.AppSearchSchema.PropertyConfig.IndexingType#INDEXING_TYPE_NONE}. + */ + public static int getMaxIndexedProperties() { + return MAX_INDEXED_PROPERTIES; + } + + /** Contains {@link GenericDocument} basic information (uri, schemaType etc).*/ + @NonNull + final Bundle mBundle; + + /** Contains all properties in {@link GenericDocument} to support getting properties via keys.*/ + @NonNull + private final Bundle mProperties; + + @NonNull + private final String mUri; + @NonNull + private final String mSchemaType; + private final long mCreationTimestampMillis; + @Nullable + private Integer mHashCode; + + /** + * Rebuilds a {@link GenericDocument} by the a bundle. + * @param bundle Contains {@link GenericDocument} basic information (uri, schemaType etc) and + * a properties bundle contains all properties in {@link GenericDocument} to + * support getting properties via keys. + * @hide + */ + + public GenericDocument(@NonNull Bundle bundle) { + Preconditions.checkNotNull(bundle); + mBundle = bundle; + mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD)); + mUri = Preconditions.checkNotNull(mBundle.getString(URI_FIELD)); + mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD)); + mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD, + System.currentTimeMillis()); + } + + /** + * Creates a new {@link GenericDocument} from an existing instance. + * + * <p>This method should be only used by constructor of a subclass. + */ + protected GenericDocument(@NonNull GenericDocument document) { + this(document.mBundle); + } + + /** + * Returns the {@link Bundle} populated by this builder. + * @hide + */ + + @NonNull + public Bundle getBundle() { + return mBundle; + } + + /** Returns the URI of the {@link GenericDocument}. */ + @NonNull + public String getUri() { + return mUri; + } + + /** Returns the namespace of the {@link GenericDocument}. */ + @NonNull + public String getNamespace() { + return mBundle.getString(NAMESPACE_FIELD, DEFAULT_NAMESPACE); + } + + /** Returns the schema type of the {@link GenericDocument}. */ + @NonNull + public String getSchemaType() { + return mSchemaType; + } + + /** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */ + public long getCreationTimestampMillis() { + return mCreationTimestampMillis; + } + + /** + * Returns the TTL (Time To Live) of the {@link GenericDocument}, 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. + */ + public long getTtlMillis() { + return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS); + } + + /** + * Returns the score of the {@link GenericDocument}. + * + * <p>The score is a query-independent measure of the document's quality, relative to other + * {@link GenericDocument}s of the same type. + * + * <p>The default value is 0. + */ + public int getScore() { + return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE); + } + + /** + * Retrieves a {@link String} value by key. + * + * @param key The key to look for. + * @return The first {@link String} associated with the given key or {@code null} if there + * is no such key or the value is of a different type. + */ + @Nullable + public String getPropertyString(@NonNull String key) { + Preconditions.checkNotNull(key); + String[] propertyArray = getPropertyStringArray(key); + if (propertyArray == null || propertyArray.length == 0) { + return null; + } + warnIfSinglePropertyTooLong("String", key, propertyArray.length); + return propertyArray[0]; + } + + /** + * Retrieves a {@code long} value by key. + * + * @param key The key to look for. + * @return The first {@code long} associated with the given key or default value {@code 0} if + * there is no such key or the value is of a different type. + */ + public long getPropertyLong(@NonNull String key) { + Preconditions.checkNotNull(key); + long[] propertyArray = getPropertyLongArray(key); + if (propertyArray == null || propertyArray.length == 0) { + return 0; + } + warnIfSinglePropertyTooLong("Long", key, propertyArray.length); + return propertyArray[0]; + } + + /** + * Retrieves a {@code double} value by key. + * + * @param key The key to look for. + * @return The first {@code double} associated with the given key or default value {@code 0.0} + * if there is no such key or the value is of a different type. + */ + public double getPropertyDouble(@NonNull String key) { + Preconditions.checkNotNull(key); + double[] propertyArray = getPropertyDoubleArray(key); + if (propertyArray == null || propertyArray.length == 0) { + return 0.0; + } + warnIfSinglePropertyTooLong("Double", key, propertyArray.length); + return propertyArray[0]; + } + + /** + * Retrieves a {@code boolean} value by key. + * + * @param key The key to look for. + * @return The first {@code boolean} associated with the given key or default value + * {@code false} if there is no such key or the value is of a different type. + */ + public boolean getPropertyBoolean(@NonNull String key) { + Preconditions.checkNotNull(key); + boolean[] propertyArray = getPropertyBooleanArray(key); + if (propertyArray == null || propertyArray.length == 0) { + return false; + } + warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length); + return propertyArray[0]; + } + + /** + * Retrieves 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) { + Preconditions.checkNotNull(key); + byte[][] propertyArray = getPropertyBytesArray(key); + if (propertyArray == null || propertyArray.length == 0) { + return null; + } + warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length); + return propertyArray[0]; + } + + /** + * Retrieves a {@link GenericDocument} value by key. + * + * @param key The key to look for. + * @return The first {@link GenericDocument} associated with the given key or {@code null} if + * there is no such key or the value is of a different type. + */ + @Nullable + public GenericDocument getPropertyDocument(@NonNull String key) { + Preconditions.checkNotNull(key); + GenericDocument[] propertyArray = getPropertyDocumentArray(key); + if (propertyArray == null || propertyArray.length == 0) { + return null; + } + warnIfSinglePropertyTooLong("Document", 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) { + if (propertyLength > 1) { + Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength + + " elements. Only the first one will be returned from " + + "getProperty" + propertyType + "(). Try getProperty" + propertyType + + "Array()."); + } + } + + /** + * Retrieves a repeated {@code String} property by key. + * + * @param key The key to look for. + * @return The {@code String[]} associated with the given key, or {@code null} if no value + * is set or the value is of a different type. + */ + @Nullable + public String[] getPropertyStringArray(@NonNull String key) { + Preconditions.checkNotNull(key); + return getAndCastPropertyArray(key, String[].class); + } + + /** + * Retrieves a repeated {@link String} property by key. + * + * @param key The key to look for. + * @return The {@code long[]} associated with the given key, or {@code null} if no value is + * set or the value is of a different type. + */ + @Nullable + public long[] getPropertyLongArray(@NonNull String key) { + Preconditions.checkNotNull(key); + return getAndCastPropertyArray(key, long[].class); + } + + /** + * Retrieves a repeated {@code double} property by key. + * + * @param key The key to look for. + * @return The {@code double[]} associated with the given key, or {@code null} if no value + * is set or the value is of a different type. + */ + @Nullable + public double[] getPropertyDoubleArray(@NonNull String key) { + Preconditions.checkNotNull(key); + return getAndCastPropertyArray(key, double[].class); + } + + /** + * Retrieves a repeated {@code boolean} property by key. + * + * @param key The key to look for. + * @return The {@code boolean[]} associated with the given key, or {@code null} if no value + * is set or the value is of a different type. + */ + @Nullable + public boolean[] getPropertyBooleanArray(@NonNull String key) { + Preconditions.checkNotNull(key); + return getAndCastPropertyArray(key, boolean[].class); + } + + /** + * Retrieves 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. + */ + @SuppressLint("ArrayReturn") + @Nullable + @SuppressWarnings("unchecked") + public byte[][] getPropertyBytesArray(@NonNull String key) { + Preconditions.checkNotNull(key); + ArrayList<Bundle> bundles = getAndCastPropertyArray(key, ArrayList.class); + if (bundles == null || bundles.size() == 0) { + return null; + } + byte[][] bytes = new byte[bundles.size()][]; + for (int i = 0; i < bundles.size(); i++) { + Bundle bundle = bundles.get(i); + if (bundle == null) { + Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key); + continue; + } + byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD); + if (innerBytes == null) { + Log.e(TAG, "The bundle at " + i + " contains a null byte[]."); + continue; + } + bytes[i] = innerBytes; + } + return bytes; + } + + /** + * Retrieves a repeated {@link GenericDocument} property by key. + * + * @param key The key to look for. + * @return The {@link GenericDocument}[] associated with the given key, or {@code null} if no + * value is set or the value is of a different type. + */ + @SuppressLint("ArrayReturn") + @Nullable + public GenericDocument[] getPropertyDocumentArray(@NonNull String key) { + Preconditions.checkNotNull(key); + Bundle[] bundles = getAndCastPropertyArray(key, Bundle[].class); + if (bundles == null || bundles.length == 0) { + return null; + } + GenericDocument[] documents = new GenericDocument[bundles.length]; + for (int i = 0; i < bundles.length; i++) { + if (bundles[i] == null) { + Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key); + continue; + } + documents[i] = new GenericDocument(bundles[i]); + } + return documents; + } + + /** + * 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 = mProperties.get(key); + if (value == null) { + return null; + } + try { + return tClass.cast(value); + } catch (ClassCastException e) { + Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e); + return null; + } + } + + @Override + public boolean equals(@Nullable Object other) { + // Check only proto's equality is sufficient here since all properties in + // mProperties are ordered by keys and stored in proto. + if (this == other) { + return true; + } + if (!(other instanceof GenericDocument)) { + return false; + } + GenericDocument otherDocument = (GenericDocument) other; + return bundleEquals(this.mBundle, otherDocument.mBundle); + } + + /** + * Deeply checks two bundle is equally or not. + * <p> Two bundle will be considered equally if they contains same content. + */ + @SuppressWarnings("unchecked") + private static boolean bundleEquals(Bundle one, Bundle two) { + if (one.size() != two.size()) { + return false; + } + Set<String> keySetOne = one.keySet(); + Object valueOne; + Object valueTwo; + // Bundle inherit its equals() from Object.java, which only compare their memory address. + // We should iterate all keys and check their presents and values in both bundle. + for (String key : keySetOne) { + valueOne = one.get(key); + valueTwo = two.get(key); + if (valueOne instanceof Bundle + && valueTwo instanceof Bundle + && !bundleEquals((Bundle) valueOne, (Bundle) valueTwo)) { + return false; + } else if (valueOne == null && (valueTwo != null || !two.containsKey(key))) { + // If we call bundle.get(key) when the 'key' doesn't actually exist in the + // bundle, we'll get back a null. So make sure that both values are null and + // both keys exist in the bundle. + return false; + } else if (valueOne instanceof boolean[]) { + if (!(valueTwo instanceof boolean[]) + || !Arrays.equals((boolean[]) valueOne, (boolean[]) valueTwo)) { + return false; + } + } else if (valueOne instanceof long[]) { + if (!(valueTwo instanceof long[]) + || !Arrays.equals((long[]) valueOne, (long[]) valueTwo)) { + return false; + } + } else if (valueOne instanceof double[]) { + if (!(valueTwo instanceof double[]) + || !Arrays.equals((double[]) valueOne, (double[]) valueTwo)) { + return false; + } + } else if (valueOne instanceof Bundle[]) { + if (!(valueTwo instanceof Bundle[])) { + return false; + } + Bundle[] bundlesOne = (Bundle[]) valueOne; + Bundle[] bundlesTwo = (Bundle[]) valueTwo; + if (bundlesOne.length != bundlesTwo.length) { + return false; + } + for (int i = 0; i < bundlesOne.length; i++) { + if (!bundleEquals(bundlesOne[i], bundlesTwo[i])) { + return false; + } + } + } else if (valueOne instanceof ArrayList) { + if (!(valueTwo instanceof ArrayList)) { + return false; + } + ArrayList<Bundle> bundlesOne = (ArrayList<Bundle>) valueOne; + ArrayList<Bundle> bundlesTwo = (ArrayList<Bundle>) valueTwo; + if (bundlesOne.size() != bundlesTwo.size()) { + return false; + } + for (int i = 0; i < bundlesOne.size(); i++) { + if (!bundleEquals(bundlesOne.get(i), bundlesTwo.get(i))) { + return false; + } + } + } else if (valueOne instanceof Object[]) { + if (!(valueTwo instanceof Object[]) + || !Arrays.equals((Object[]) valueOne, (Object[]) valueTwo)) { + return false; + } + } + } + return true; + } + + @Override + public int hashCode() { + if (mHashCode == null) { + mHashCode = bundleHashCode(mBundle); + } + return mHashCode; + } + + /** + * Calculates the hash code for a bundle. + * <p> The hash code is only effected by the content in the bundle. Bundles will get + * consistent hash code if they have same content. + */ + @SuppressWarnings("unchecked") + private static int bundleHashCode(Bundle bundle) { + int[] hashCodes = new int[bundle.size()]; + int i = 0; + // Bundle inherit its hashCode() from Object.java, which only relative to their memory + // address. Bundle doesn't have an order, so we should iterate all keys and combine + // their value's hashcode into an array. And use the hashcode of the array to be + // the hashcode of the bundle. + for (String key : bundle.keySet()) { + Object value = bundle.get(key); + if (value instanceof boolean[]) { + hashCodes[i++] = Arrays.hashCode((boolean[]) value); + } else if (value instanceof long[]) { + hashCodes[i++] = Arrays.hashCode((long[]) value); + } else if (value instanceof double[]) { + hashCodes[i++] = Arrays.hashCode((double[]) value); + } else if (value instanceof String[]) { + hashCodes[i++] = Arrays.hashCode((Object[]) value); + } else if (value instanceof Bundle) { + hashCodes[i++] = bundleHashCode((Bundle) value); + } else if (value instanceof Bundle[]) { + Bundle[] bundles = (Bundle[]) value; + int[] innerHashCodes = new int[bundles.length]; + for (int j = 0; j < innerHashCodes.length; j++) { + innerHashCodes[j] = bundleHashCode(bundles[j]); + } + hashCodes[i++] = Arrays.hashCode(innerHashCodes); + } else if (value instanceof ArrayList) { + ArrayList<Bundle> bundles = (ArrayList<Bundle>) value; + int[] innerHashCodes = new int[bundles.size()]; + for (int j = 0; j < innerHashCodes.length; j++) { + innerHashCodes[j] = bundleHashCode(bundles.get(j)); + } + hashCodes[i++] = Arrays.hashCode(innerHashCodes); + } else { + hashCodes[i++] = value.hashCode(); + } + } + return Arrays.hashCode(hashCodes); + } + + @Override + @NonNull + public String toString() { + return bundleToString(mBundle).toString(); + } + + @SuppressWarnings("unchecked") + private static StringBuilder bundleToString(Bundle bundle) { + StringBuilder stringBuilder = new StringBuilder(); + try { + final Set<String> keySet = bundle.keySet(); + String[] keys = keySet.toArray(new String[0]); + // Sort keys to make output deterministic. We need a custom comparator to handle + // nulls (arbitrarily putting them first, similar to Comparator.nullsFirst, which is + // only available since N). + Arrays.sort( + keys, + (@Nullable String s1, @Nullable String s2) -> { + if (s1 == null) { + return s2 == null ? 0 : -1; + } else if (s2 == null) { + return 1; + } else { + return s1.compareTo(s2); + } + }); + for (String key : keys) { + stringBuilder.append("{ key: '").append(key).append("' value: "); + Object valueObject = bundle.get(key); + if (valueObject == null) { + stringBuilder.append("<null>"); + } else if (valueObject instanceof Bundle) { + stringBuilder.append(bundleToString((Bundle) valueObject)); + } else if (valueObject.getClass().isArray()) { + stringBuilder.append("[ "); + for (int i = 0; i < Array.getLength(valueObject); i++) { + Object element = Array.get(valueObject, i); + stringBuilder.append("'"); + if (element instanceof Bundle) { + stringBuilder.append(bundleToString((Bundle) element)); + } else { + stringBuilder.append(Array.get(valueObject, i)); + } + stringBuilder.append("' "); + } + stringBuilder.append("]"); + } else if (valueObject instanceof ArrayList) { + for (Bundle innerBundle : (ArrayList<Bundle>) valueObject) { + stringBuilder.append(bundleToString(innerBundle)); + } + } else { + stringBuilder.append(valueObject.toString()); + } + stringBuilder.append(" } "); + } + } catch (RuntimeException e) { + // Catch any exceptions here since corrupt Bundles can throw different types of + // exceptions (e.g. b/38445840 & b/68937025). + stringBuilder.append("<error>"); + } + return stringBuilder; + } + + /** + * The builder class for {@link GenericDocument}. + * + * @param <BuilderType> Type of subclass who extend this. + */ + public static class Builder<BuilderType extends Builder> { + + private final Bundle mProperties = new Bundle(); + private final Bundle mBundle = new Bundle(); + private final BuilderType mBuilderTypeInstance; + private boolean mBuilt = false; + + /** + * Create a new {@link GenericDocument.Builder}. + * + * @param uri The uri of {@link GenericDocument}. + * @param schemaType The schema type of the {@link GenericDocument}. The passed-in + * {@code schemaType} must be defined using {@code AppSearchManager#setSchema} prior + * to inserting a document of this {@code schemaType} into the AppSearch index using + * {@code AppSearchManager#putDocuments}. Otherwise, the document will be + * rejected by {@code AppSearchManager#putDocuments}. + */ + //TODO(b/157082794) Linkify AppSearchManager once that API is public. + @SuppressWarnings("unchecked") + public Builder(@NonNull String uri, @NonNull String schemaType) { + Preconditions.checkNotNull(uri); + Preconditions.checkNotNull(schemaType); + mBuilderTypeInstance = (BuilderType) this; + mBundle.putString(GenericDocument.URI_FIELD, uri); + mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType); + mBundle.putString(GenericDocument.NAMESPACE_FIELD, DEFAULT_NAMESPACE); + // Set current timestamp for creation timestamp by default. + mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, + System.currentTimeMillis()); + mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS); + mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE); + mBundle.putBundle(PROPERTIES_FIELD, mProperties); + } + + /** + * Set the app-defined namespace this Document resides in. No special values are + * reserved or understood by the infrastructure. URIs are unique within a namespace. The + * number of namespaces per app should be kept small for efficiency reasons. + */ + @NonNull + public BuilderType setNamespace(@NonNull String namespace) { + mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace); + return mBuilderTypeInstance; + } + + /** + * Sets the score of the {@link GenericDocument}. + * + * <p>The score is a query-independent measure of the document's quality, relative to + * other {@link GenericDocument}s of the same type. + * + * @throws IllegalArgumentException If the provided value is negative. + */ + @NonNull + public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + if (score < 0) { + throw new IllegalArgumentException("Document score cannot be negative."); + } + mBundle.putInt(GenericDocument.SCORE_FIELD, score); + return mBuilderTypeInstance; + } + + /** + * Set the creation timestamp in milliseconds of the {@link GenericDocument}. Should be + * set using a value obtained from the {@link System#currentTimeMillis()} time base. + */ + @NonNull + public BuilderType setCreationTimestampMillis(long creationTimestampMillis) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, + creationTimestampMillis); + return mBuilderTypeInstance; + } + + /** + * Set the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds. + * + * <p>After this many milliseconds since the {@link #setCreationTimestampMillis 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(long ttlMillis) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + if (ttlMillis < 0) { + throw new IllegalArgumentException("Document ttlMillis cannot be negative."); + } + mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis); + return mBuilderTypeInstance; + } + + /** + * Sets one or multiple {@code String} values for a property, replacing its previous + * values. + * + * @param key The key associated with the {@code values}. + * @param values The {@code String} values of the property. + */ + @NonNull + public BuilderType setProperty(@NonNull String key, @NonNull String... values) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(values); + putInPropertyBundle(key, values); + return mBuilderTypeInstance; + } + + /** + * Sets one or multiple {@code boolean} values for a property, replacing its previous + * values. + * + * @param key The key associated with the {@code values}. + * @param values The {@code boolean} values of the property. + */ + @NonNull + public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(values); + putInPropertyBundle(key, values); + return mBuilderTypeInstance; + } + + /** + * Sets one or multiple {@code long} values for a property, replacing its previous + * values. + * + * @param key The key associated with the {@code values}. + * @param values The {@code long} values of the property. + */ + @NonNull + public BuilderType setProperty(@NonNull String key, @NonNull long... values) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(values); + putInPropertyBundle(key, values); + return mBuilderTypeInstance; + } + + /** + * Sets one or multiple {@code double} values for a property, replacing its previous + * values. + * + * @param key The key associated with the {@code values}. + * @param values The {@code double} values of the property. + */ + @NonNull + public BuilderType setProperty(@NonNull String key, @NonNull double... values) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(values); + putInPropertyBundle(key, values); + return mBuilderTypeInstance; + } + + /** + * 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) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(values); + putInPropertyBundle(key, values); + return mBuilderTypeInstance; + } + + /** + * Sets one or multiple {@link GenericDocument} values for a property, replacing its + * previous values. + * + * @param key The key associated with the {@code values}. + * @param values The {@link GenericDocument} values of the property. + */ + @NonNull + public BuilderType setProperty(@NonNull String key, @NonNull GenericDocument... values) { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(values); + putInPropertyBundle(key, values); + return mBuilderTypeInstance; + } + + private void putInPropertyBundle(@NonNull String key, @NonNull String[] values) + throws IllegalArgumentException { + validateRepeatedPropertyLength(key, values.length); + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("The String at " + i + " is null."); + } else if (values[i].length() > MAX_STRING_LENGTH) { + throw new IllegalArgumentException("The String at " + i + " length is: " + + values[i].length() + ", which exceeds length limit: " + + MAX_STRING_LENGTH + "."); + } + } + mProperties.putStringArray(key, values); + } + + private void putInPropertyBundle(@NonNull String key, @NonNull boolean[] values) { + validateRepeatedPropertyLength(key, values.length); + mProperties.putBooleanArray(key, values); + } + + private void putInPropertyBundle(@NonNull String key, @NonNull double[] values) { + validateRepeatedPropertyLength(key, values.length); + mProperties.putDoubleArray(key, values); + } + + private void putInPropertyBundle(@NonNull String key, @NonNull long[] values) { + validateRepeatedPropertyLength(key, values.length); + mProperties.putLongArray(key, values); + } + + /** + * Converts and saves a byte[][] into {@link #mProperties}. + * + * <p>Bundle doesn't support for two dimension array byte[][], we are converting byte[][] + * into ArrayList<Bundle>, and each elements will contain a one dimension byte[]. + */ + private void putInPropertyBundle(@NonNull String key, @NonNull byte[][] values) { + validateRepeatedPropertyLength(key, values.length); + ArrayList<Bundle> bundles = new ArrayList<>(values.length); + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("The byte[] at " + i + " is null."); + } + Bundle bundle = new Bundle(); + bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]); + bundles.add(bundle); + } + mProperties.putParcelableArrayList(key, bundles); + } + + private void putInPropertyBundle(@NonNull String key, @NonNull GenericDocument[] values) { + validateRepeatedPropertyLength(key, values.length); + Bundle[] documentBundles = new Bundle[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("The document at " + i + " is null."); + } + documentBundles[i] = values[i].mBundle; + } + mProperties.putParcelableArray(key, documentBundles); + } + + private static void validateRepeatedPropertyLength(@NonNull String key, int length) { + if (length == 0) { + throw new IllegalArgumentException("The input array is empty."); + } else if (length > MAX_REPEATED_PROPERTY_LENGTH) { + throw new IllegalArgumentException( + "Repeated property \"" + key + "\" has length " + length + + ", which exceeds the limit of " + + MAX_REPEATED_PROPERTY_LENGTH); + } + } + + /** Builds the {@link GenericDocument} object. */ + @NonNull + public GenericDocument build() { + Preconditions.checkState(!mBuilt, "Builder has already been used"); + mBuilt = true; + return new GenericDocument(mBundle); + } + } +} diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index c710a29919e3..8e18346ffece 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -21,6 +21,7 @@ import com.android.internal.infra.AndroidFuture; parcelable AppSearchResult; parcelable AppSearchBatchResult; +parcelable SearchResults; /** {@hide} */ interface IAppSearchManager { @@ -41,7 +42,7 @@ interface IAppSearchManager { /** * Inserts documents into the index. * - * @param documentsBytes {@link List}<byte[]> of serialized DocumentProtos. + * @param documentBundes List of GenericDocument bundles. * @param callback * {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link Void}>>. * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, @@ -49,18 +50,19 @@ interface IAppSearchManager { * {@link AppSearchBatchResult}<{@link String}, {@link Void}> * where the keys are document URIs, and the values are {@code null}. */ - void putDocuments(in List documentsBytes, in AndroidFuture<AppSearchBatchResult> callback); + void putDocuments( + in List<Bundle> documentBundles, in AndroidFuture<AppSearchBatchResult> callback); /** * Retrieves documents from the index. * * @param uris The URIs of the documents to retrieve * @param callback - * {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link byte[]}>>. + * {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link Bundle}>>. * If the call fails to start, {@code callback} will be completed exceptionally. Otherwise, * {@code callback} will be completed with an - * {@link AppSearchBatchResult}<{@link String}, {@link byte[]}> - * where the keys are document URIs, and the values are serialized Document protos. + * {@link AppSearchBatchResult}<{@link String}, {@link Bundle}> + * where the keys are document URIs, and the values are Document bundles. */ void getDocuments(in List<String> uris, in AndroidFuture<AppSearchBatchResult> callback); @@ -69,8 +71,7 @@ interface IAppSearchManager { * * @param queryExpression String to search for * @param searchSpecBundle SearchSpec bundle - * @param callback {@link AndroidFuture}<{@link AppSearchResult}<{@link byte[]}>> - * Will be completed with a serialized {@link SearchResultsProto}. + * @param callback {@link AndroidFuture}<{@link AppSearchResult}<{@link SearchResults}>> */ void query( in String queryExpression, diff --git a/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java b/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java deleted file mode 100644 index 5ce296082d70..000000000000 --- a/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2020 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; - -import android.annotation.NonNull; -import android.util.Range; - -import com.google.android.icing.proto.SnippetMatchProto; - -/** - * Snippet: It refers to a substring of text from the content of document that is returned as a - * part of search result. - * This class represents a match objects for any Snippets that might be present in - * {@link SearchResults} from query. Using this class user can get the full text, exact matches and - * Snippets of document content for a given match. - * - * <p>Class Example 1: - * A document contains following text in property subject: - * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar. - * - * <p>If the queryExpression is "foo". - * - * <p>{@link MatchInfo#getPropertyPath()} returns "subject" - * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another nonsense - * word that’s used a lot is bar." - * <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32] - * <p>{@link MatchInfo#getExactMatch()} returns "foo" - * <p>{@link MatchInfo#getSnippetPosition()} returns [29, 41] - * <p>{@link MatchInfo#getSnippet()} returns "is foo. Another" - * <p> - * <p>Class Example 2: - * A document contains a property name sender which contains 2 property names name and email, so - * we will have 2 property paths: {@code sender.name} and {@code sender.email}. - * <p> Let {@code sender.name = "Test Name Jr."} and {@code sender.email = "TestNameJr@gmail.com"} - * - * <p>If the queryExpression is "Test". We will have 2 matches. - * - * <p> Match-1 - * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name" - * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr." - * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4] - * <p>{@link MatchInfo#getExactMatch()} returns "Test" - * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9] - * <p>{@link MatchInfo#getSnippet()} returns "Test Name Jr." - * <p> Match-2 - * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email" - * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com" - * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20] - * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com" - * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20] - * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com" - * @hide - */ -// TODO(sidchhabra): Capture real snippet after integration with icingLib. -public final class MatchInfo { - - private final String mPropertyPath; - private final SnippetMatchProto mSnippetMatch; - private final AppSearchDocument mDocument; - /** - * List of content with same property path in a document when there are multiple matches in - * repeated sections. - */ - private final String[] mValues; - - /** @hide */ - public MatchInfo(@NonNull String propertyPath, @NonNull SnippetMatchProto snippetMatch, - @NonNull AppSearchDocument document) { - mPropertyPath = propertyPath; - mSnippetMatch = snippetMatch; - mDocument = document; - // In IcingLib snippeting is available for only 3 data types i.e String, double and long, - // so we need to check which of these three are requested. - // TODO (sidchhabra): getPropertyStringArray takes property name, handle for property path. - String[] values = mDocument.getPropertyStringArray(propertyPath); - if (values == null) { - values = doubleToString(mDocument.getPropertyDoubleArray(propertyPath)); - } - if (values == null) { - values = longToString(mDocument.getPropertyLongArray(propertyPath)); - } - if (values == null) { - throw new IllegalStateException("No content found for requested property path!"); - } - mValues = values; - } - - /** - * Gets the property path corresponding to the given entry. - * <p>Property Path: '.' - delimited sequence of property names indicating which property in - * the Document these snippets correspond to. - * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. - * For class example 1 this returns "subject" - */ - @NonNull - public String getPropertyPath() { - return mPropertyPath; - } - - /** - * Gets the full text corresponding to the given entry. - * <p>For class example this returns "A commonly used fake word is foo. Another nonsense word - * that’s used a lot is bar." - */ - @NonNull - public String getFullText() { - return mValues[mSnippetMatch.getValuesIndex()]; - } - - /** - * Gets the exact match range corresponding to the given entry. - * <p>For class example 1 this returns [29, 32] - */ - @NonNull - public Range getExactMatchPosition() { - return new Range(mSnippetMatch.getExactMatchPosition(), - mSnippetMatch.getExactMatchPosition() + mSnippetMatch.getExactMatchBytes()); - } - - /** - * Gets the exact match corresponding to the given entry. - * <p>For class example 1 this returns "foo" - */ - @NonNull - public CharSequence getExactMatch() { - return getSubstring(getExactMatchPosition()); - } - - /** - * Gets the snippet range corresponding to the given entry. - * <p>For class example 1 this returns [29, 41] - */ - @NonNull - public Range getSnippetPosition() { - return new Range(mSnippetMatch.getWindowPosition(), - mSnippetMatch.getWindowPosition() + mSnippetMatch.getWindowBytes()); - } - - /** - * Gets the snippet corresponding to the given entry. - * <p>Snippet - Provides a subset of the content to display. The - * length of this content can be changed {@link SearchSpec.Builder#setMaxSnippetSize(int)}. - * Windowing is centered around the middle of the matched token with content on either side - * clipped to token boundaries. - * <p>For class example 1 this returns "foo. Another" - */ - @NonNull - public CharSequence getSnippet() { - return getSubstring(getSnippetPosition()); - } - - private CharSequence getSubstring(Range range) { - return getFullText() - .substring((int) range.getLower(), (int) range.getUpper()); - } - - /** Utility method to convert double[] to String[] */ - private String[] doubleToString(double[] values) { - //TODO(sidchhabra): Implement the method. - return null; - } - - /** Utility method to convert long[] to String[] */ - private String[] longToString(long[] values) { - //TODO(sidchhabra): Implement the method. - return null; - } -} diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResult.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResult.java new file mode 100644 index 000000000000..758280bbc322 --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResult.java @@ -0,0 +1,368 @@ +/* + * Copyright 2020 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; + +import android.os.Bundle; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.util.Objects; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class represents one of the results obtained from the query. + * + * <p>It contains the document which matched, information about which section(s) in the document + * matched, and snippet information containing textual summaries of the document's match(es). + * @hide + */ +public final class SearchResult { + /** @hide */ + + public static final String DOCUMENT_FIELD = "document"; + + /** @hide */ + + public static final String MATCHES_FIELD = "matches"; + + @NonNull + private final Bundle mBundle; + + @NonNull + private final Bundle mDocumentBundle; + + @Nullable + private GenericDocument mDocument; + + @Nullable + private final List<Bundle> mMatchBundles; + + /** + * Contains a list of Snippets that matched the request. Only populated when requested in + * both {@link SearchSpec.Builder#setSnippetCount(int)} + * and {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}. + * + * @see #getMatches() + */ + @Nullable + private List<MatchInfo> mMatches; + + /** @hide */ + + public SearchResult(@NonNull Bundle bundle) { + mBundle = Preconditions.checkNotNull(bundle); + mDocumentBundle = Preconditions.checkNotNull(bundle.getBundle(DOCUMENT_FIELD)); + mMatchBundles = bundle.getParcelableArrayList(MATCHES_FIELD); + } + + /** @hide */ + + @NonNull + public Bundle getBundle() { + return mBundle; + } + + /** + * Contains the matching {@link GenericDocument}. + * @return Document object which matched the query. + */ + @NonNull + public GenericDocument getDocument() { + if (mDocument == null) { + mDocument = new GenericDocument(mDocumentBundle); + } + return mDocument; + } + + /** + * Contains a list of Snippets that matched the request. Only populated when requested in + * both {@link SearchSpec.Builder#setSnippetCount(int)} + * and {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}. + * + * @return List of matches based on {@link SearchSpec}, if snippeting is disabled and this + * method is called it will return {@code null}. Users can also restrict snippet population + * using {@link SearchSpec.Builder#setSnippetCount} and + * {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}, for all results after that + * value this method will return {@code null}. + */ + @Nullable + public List<MatchInfo> getMatches() { + if (mMatchBundles != null && mMatches == null) { + mMatches = new ArrayList<>(mMatchBundles.size()); + for (int i = 0; i < mMatchBundles.size(); i++) { + MatchInfo matchInfo = new MatchInfo(getDocument(), mMatchBundles.get(i)); + mMatches.add(matchInfo); + } + } + return mMatches; + } + + /** + * Snippet: It refers to a substring of text from the content of document that is returned as a + * part of search result. + * This class represents a match objects for any Snippets that might be present in + * {@link SearchResults} from query. Using this class user can get the full text, exact matches + * and Snippets of document content for a given match. + * + * <p>Class Example 1: + * A document contains following text in property subject: + * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar. + * + * <p>If the queryExpression is "foo". + * + * <p>{@link MatchInfo#getPropertyPath()} returns "subject" + * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another + * nonsense word that’s used a lot is bar." + * <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32] + * <p>{@link MatchInfo#getExactMatch()} returns "foo" + * <p>{@link MatchInfo#getSnippetPosition()} returns [26, 33] + * <p>{@link MatchInfo#getSnippet()} returns "is foo." + * <p> + * <p>Class Example 2: + * A document contains a property name sender which contains 2 property names name and email, so + * we will have 2 property paths: {@code sender.name} and {@code sender.email}. + * <p>Let {@code sender.name = "Test Name Jr."} and + * {@code sender.email = "TestNameJr@gmail.com"} + * + * <p>If the queryExpression is "Test". We will have 2 matches. + * + * <p> Match-1 + * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name" + * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr." + * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4] + * <p>{@link MatchInfo#getExactMatch()} returns "Test" + * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9] + * <p>{@link MatchInfo#getSnippet()} returns "Test Name" + * <p> Match-2 + * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email" + * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com" + * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20] + * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com" + * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20] + * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com" + */ + public static final class MatchInfo { + /** + * The path of the matching snippet property. + * @hide + */ + + public static final String PROPERTY_PATH_FIELD = "propertyPath"; + + /** + * The index of matching value in its property. A property may have multiple values. This + * index indicates which value is the match. + * @hide + */ + + public static final String VALUES_INDEX_FIELD = "valuesIndex"; + + /** @hide */ + + public static final String EXACT_MATCH_POSITION_LOWER_FIELD = "exactMatchPositionLower"; + + /** @hide */ + + public static final String EXACT_MATCH_POSITION_UPPER_FIELD = "exactMatchPositionUpper"; + + /** @hide */ + + public static final String WINDOW_POSITION_LOWER_FIELD = "windowPositionLower"; + + /** @hide */ + + public static final String WINDOW_POSITION_UPPER_FIELD = "windowPositionUpper"; + + private final String mFullText; + private final String mPropertyPath; + private final Bundle mBundle; + private MatchRange mExactMatchRange; + private MatchRange mWindowRange; + + MatchInfo(@NonNull GenericDocument document, @NonNull Bundle bundle) { + mBundle = Preconditions.checkNotNull(bundle); + Preconditions.checkNotNull(document); + mPropertyPath = Preconditions.checkNotNull(bundle.getString(PROPERTY_PATH_FIELD)); + mFullText = getPropertyValues( + document, mPropertyPath, mBundle.getInt(VALUES_INDEX_FIELD)); + } + + /** + * Gets the property path corresponding to the given entry. + * <p>Property Path: '.' - delimited sequence of property names indicating which property in + * the Document these snippets correspond to. + * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. + * For class example 1 this returns "subject" + */ + @NonNull + public String getPropertyPath() { + return mPropertyPath; + } + + /** + * Gets the full text corresponding to the given entry. + * <p>For class example this returns "A commonly used fake word is foo. Another nonsense + * word that's used a lot is bar." + */ + @NonNull + public String getFullText() { + return mFullText; + } + + /** + * Gets the exact {@link MatchRange} corresponding to the given entry. + * <p>For class example 1 this returns [29, 32] + */ + @NonNull + public MatchRange getExactMatchPosition() { + if (mExactMatchRange == null) { + mExactMatchRange = new MatchRange( + mBundle.getInt(EXACT_MATCH_POSITION_LOWER_FIELD), + mBundle.getInt(EXACT_MATCH_POSITION_UPPER_FIELD)); + } + return mExactMatchRange; + } + + /** + * Gets the {@link MatchRange} corresponding to the given entry. + * <p>For class example 1 this returns "foo" + */ + @NonNull + public CharSequence getExactMatch() { + return getSubstring(getExactMatchPosition()); + } + + /** + * Gets the snippet {@link MatchRange} corresponding to the given entry. + * <p>Only populated when set maxSnippetSize > 0 in + * {@link SearchSpec.Builder#setMaxSnippetSize}. + * <p>For class example 1 this returns [29, 41]. + */ + @NonNull + public MatchRange getSnippetPosition() { + if (mWindowRange == null) { + mWindowRange = new MatchRange( + mBundle.getInt(WINDOW_POSITION_LOWER_FIELD), + mBundle.getInt(WINDOW_POSITION_UPPER_FIELD)); + } + return mWindowRange; + } + + /** + * Gets the snippet corresponding to the given entry. + * <p>Snippet - Provides a subset of the content to display. Only populated when requested + * maxSnippetSize > 0. The size of this content can be changed by + * {@link SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of + * the matched token with content on either side clipped to token boundaries. + * <p>For class example 1 this returns "foo. Another" + */ + @NonNull + public CharSequence getSnippet() { + return getSubstring(getSnippetPosition()); + } + + private CharSequence getSubstring(MatchRange range) { + return getFullText().substring(range.getStart(), range.getEnd()); + } + + /** Extracts the matching string from the document. */ + private static String getPropertyValues( + GenericDocument document, String propertyName, int valueIndex) { + // In IcingLib snippeting is available for only 3 data types i.e String, double and + // long, so we need to check which of these three are requested. + // TODO (tytytyww): getPropertyStringArray takes property name, handle for property + // path. + // TODO (tytytyww): support double[] and long[]. + String[] values = document.getPropertyStringArray(propertyName); + if (values == null) { + throw new IllegalStateException("No content found for requested property path!"); + } + return values[valueIndex]; + } + } + + /** + * Class providing the position range of matching information. + * + * <p> All ranges are finite, and the left side of the range is always {@code <=} the right + * side of the range. + * + * <p> Example: MatchRange(0, 100) represent a hundred ints from 0 to 99." + * + */ + public static final class MatchRange { + private final int mEnd; + private final int mStart; + + /** + * Creates a new immutable range. + * <p> The endpoints are {@code [start, end)}; that is the range is bounded. {@code start} + * must be lesser or equal to {@code end}. + * + * @param start The start point (inclusive) + * @param end The end point (exclusive) + * @hide + */ + + public MatchRange(int start, int end) { + if (start > end) { + throw new IllegalArgumentException("Start point must be less than or equal to " + + "end point"); + } + mStart = start; + mEnd = end; + } + + /** Gets the start point (inclusive). */ + public int getStart() { + return mStart; + } + + /** Gets the end point (exclusive). */ + public int getEnd() { + return mEnd; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MatchRange)) { + return false; + } + MatchRange otherMatchRange = (MatchRange) other; + return this.getStart() == otherMatchRange.getStart() + && this.getEnd() == otherMatchRange.getEnd(); + } + + @Override + @NonNull + public String toString() { + return "MatchRange { start: " + mStart + " , end: " + mEnd + "}"; + } + + @Override + public int hashCode() { + return Objects.hash(mStart, mEnd); + } + } +} diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java index 7287fe68f519..9f376250f1a6 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java @@ -17,112 +17,62 @@ package android.app.appsearch; import android.annotation.NonNull; -import android.annotation.Nullable; - -import com.google.android.icing.proto.SearchResultProto; -import com.google.android.icing.proto.SnippetMatchProto; -import com.google.android.icing.proto.SnippetProto; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.NoSuchElementException; /** - * SearchResults are a list of results that are returned from a query. Each result from this - * list contains a document and may contain other fields like snippets based on request. - * This iterator class is not thread safe. + * Structure for transmitting a page of search results across binder. * @hide */ -public final class SearchResults implements Iterator<SearchResults.Result> { - - private final SearchResultProto mSearchResultProto; - private int mNextIdx; +public final class SearchResults implements Parcelable { + final List<SearchResult> mResults; + final long mNextPageToken; - /** @hide */ - public SearchResults(SearchResultProto searchResultProto) { - mSearchResultProto = searchResultProto; + public SearchResults(@NonNull List<SearchResult> results, long nextPageToken) { + mResults = results; + mNextPageToken = nextPageToken; } - @Override - public boolean hasNext() { - return mNextIdx < mSearchResultProto.getResultsCount(); + private SearchResults(@NonNull Parcel in) { + List<Bundle> resultBundles = in.readArrayList(/*loader=*/ null); + mResults = new ArrayList<>(resultBundles.size()); + for (int i = 0; i < resultBundles.size(); i++) { + SearchResult searchResult = new SearchResult(resultBundles.get(i)); + mResults.add(searchResult); + } + mNextPageToken = in.readLong(); } - @NonNull @Override - public Result next() { - if (!hasNext()) { - throw new NoSuchElementException(); + public void writeToParcel(@NonNull Parcel dest, int flags) { + List<Bundle> resultBundles = new ArrayList<>(mResults.size()); + for (int i = 0; i < mResults.size(); i++) { + resultBundles.add(mResults.get(i).getBundle()); } - Result result = new Result(mSearchResultProto.getResults(mNextIdx)); - mNextIdx++; - return result; + dest.writeList(resultBundles); + dest.writeLong(mNextPageToken); } + @Override + public int describeContents() { + return 0; + } - - /** - * This class represents the result obtained from the query. It will contain the document which - * which matched the specified query string and specifications. - * @hide - */ - public static final class Result { - private final SearchResultProto.ResultProto mResultProto; - - @Nullable - private AppSearchDocument mDocument; - - private Result(SearchResultProto.ResultProto resultProto) { - mResultProto = resultProto; - } - - /** - * Contains the matching {@link AppSearchDocument}. - * @return Document object which matched the query. - * @hide - */ + public static final Creator<SearchResults> CREATOR = new Creator<SearchResults>() { @NonNull - public AppSearchDocument getDocument() { - if (mDocument == null) { - mDocument = new AppSearchDocument(mResultProto.getDocument()); - } - return mDocument; + @Override + public SearchResults createFromParcel(@NonNull Parcel in) { + return new SearchResults(in); } - /** - * Contains a list of Snippets that matched the request. Only populated when requested in - * {@link SearchSpec.Builder#setMaxSnippetSize(int)}. - * @return List of matches based on {@link SearchSpec}, if snippeting is disabled and this - * method is called it will return {@code null}. Users can also restrict snippet population - * using {@link SearchSpec.Builder#setNumToSnippet} and - * {@link SearchSpec.Builder#setNumMatchesPerProperty}, for all results after that value - * this method will return {@code null}. - * @hide - */ - // TODO(sidchhabra): Replace Document with proper constructor. - @Nullable - public List<MatchInfo> getMatchInfo() { - if (!mResultProto.hasSnippet()) { - return null; - } - AppSearchDocument document = getDocument(); - List<MatchInfo> matchList = new ArrayList<>(); - for (Iterator entryProtoIterator = mResultProto.getSnippet() - .getEntriesList().iterator(); entryProtoIterator.hasNext(); ) { - SnippetProto.EntryProto entry = (SnippetProto.EntryProto) entryProtoIterator.next(); - for (Iterator snippetMatchProtoIterator = entry.getSnippetMatchesList().iterator(); - snippetMatchProtoIterator.hasNext(); ) { - matchList.add(new MatchInfo(entry.getPropertyName(), - (SnippetMatchProto) snippetMatchProtoIterator.next(), document)); - } - } - return matchList; + @NonNull + @Override + public SearchResults[] newArray(int size) { + return new SearchResults[size]; } - } - - @Override - public String toString() { - return mSearchResultProto.toString(); - } + }; } diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java index 817c3ef5e028..c8719059fa8c 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java @@ -249,8 +249,10 @@ public final class SearchSpec { /** * Only the first {@code snippetCount} documents based on the ranking strategy * will have snippet information provided. - * <p>If set to 0 (default), snippeting is disabled and - * {@link SearchResults.Result#getMatches} will return {@code null} for that result. + * + * <p>If set to 0 (default), snippeting is disabled and {@link SearchResult#getMatches} will + * return {@code null} for that result. + * * <p>The value should be set in range[0, 10k]. */ @NonNull @@ -264,8 +266,10 @@ public final class SearchSpec { /** * Only the first {@code matchesCountPerProperty} matches for a every property of * {@link GenericDocument} will contain snippet information. - * <p>If set to 0, snippeting is disabled and {@link SearchResults.Result#getMatches} - * will return {@code null} for that result. + * + * <p>If set to 0, snippeting is disabled and {@link SearchResult#getMatches} will return + * {@code null} for that result. + * * <p>The value should be set in range[0, 10k]. */ @NonNull diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java index 4bc0c39bb6c9..06612ac31f63 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -17,10 +17,12 @@ package com.android.server.appsearch; import android.annotation.NonNull; import android.app.appsearch.AppSearchBatchResult; -import android.app.appsearch.AppSearchDocument; import android.app.appsearch.AppSearchResult; import android.app.appsearch.AppSearchSchema; +import android.app.appsearch.GenericDocument; import android.app.appsearch.IAppSearchManager; +import android.app.appsearch.SearchResult; +import android.app.appsearch.SearchResults; import android.app.appsearch.SearchSpec; import android.app.appsearch.exceptions.AppSearchException; import android.content.Context; @@ -32,7 +34,9 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.util.Preconditions; import com.android.server.SystemService; import com.android.server.appsearch.external.localbackend.AppSearchImpl; +import com.android.server.appsearch.external.localbackend.converter.GenericDocumentToProtoConverter; import com.android.server.appsearch.external.localbackend.converter.SchemaToProtoConverter; +import com.android.server.appsearch.external.localbackend.converter.SearchResultToProtoConverter; import com.android.server.appsearch.external.localbackend.converter.SearchSpecToProtoConverter; import com.google.android.icing.proto.DocumentProto; @@ -80,7 +84,7 @@ public class AppSearchManagerService extends SystemService { AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); String databaseName = makeDatabaseName(callingUid); impl.setSchema(databaseName, schemaProtoBuilder.build(), forceOverride); - callback.complete(AppSearchResult.newSuccessfulResult(/*value=*/ null)); + callback.complete(AppSearchResult.newSuccessfulResult(/*result=*/ null)); } catch (Throwable t) { callback.complete(throwableToFailedResult(t)); } finally { @@ -90,9 +94,9 @@ public class AppSearchManagerService extends SystemService { @Override public void putDocuments( - @NonNull List documentsBytes, + @NonNull List<Bundle> documentBundles, @NonNull AndroidFuture<AppSearchBatchResult> callback) { - Preconditions.checkNotNull(documentsBytes); + Preconditions.checkNotNull(documentBundles); Preconditions.checkNotNull(callback); int callingUid = Binder.getCallingUidOrThrow(); int callingUserId = UserHandle.getUserId(callingUid); @@ -102,12 +106,12 @@ public class AppSearchManagerService extends SystemService { String databaseName = makeDatabaseName(callingUid); AppSearchBatchResult.Builder<String, Void> resultBuilder = new AppSearchBatchResult.Builder<>(); - for (int i = 0; i < documentsBytes.size(); i++) { - byte[] documentBytes = (byte[]) documentsBytes.get(i); - DocumentProto document = DocumentProto.parseFrom(documentBytes); + for (int i = 0; i < documentBundles.size(); i++) { + GenericDocument document = new GenericDocument(documentBundles.get(i)); + DocumentProto documentProto = GenericDocumentToProtoConverter.convert(document); try { - impl.putDocument(databaseName, document); - resultBuilder.setSuccess(document.getUri(), /*value=*/ null); + impl.putDocument(databaseName, documentProto); + resultBuilder.setSuccess(document.getUri(), /*result=*/ null); } catch (Throwable t) { resultBuilder.setResult(document.getUri(), throwableToFailedResult(t)); } @@ -131,18 +135,20 @@ public class AppSearchManagerService extends SystemService { try { AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); String databaseName = makeDatabaseName(callingUid); - AppSearchBatchResult.Builder<String, byte[]> resultBuilder = + AppSearchBatchResult.Builder<String, Bundle> resultBuilder = new AppSearchBatchResult.Builder<>(); for (int i = 0; i < uris.size(); i++) { String uri = uris.get(i); try { - DocumentProto document = impl.getDocument( - databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri); - if (document == null) { + DocumentProto documentProto = impl.getDocument( + databaseName, GenericDocument.DEFAULT_NAMESPACE, uri); + if (documentProto == null) { resultBuilder.setFailure( uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null); } else { - resultBuilder.setSuccess(uri, document.toByteArray()); + GenericDocument genericDocument = + GenericDocumentToProtoConverter.convert(documentProto); + resultBuilder.setSuccess(uri, genericDocument.getBundle()); } } catch (Throwable t) { resultBuilder.setResult(uri, throwableToFailedResult(t)); @@ -182,8 +188,11 @@ public class AppSearchManagerService extends SystemService { searchSpecProto, SearchSpecToProtoConverter.toResultSpecProto(searchSpec), SearchSpecToProtoConverter.toScoringSpecProto(searchSpec)); - callback.complete( - AppSearchResult.newSuccessfulResult(searchResultProto.toByteArray())); + List<SearchResult> searchResultList = + SearchResultToProtoConverter.convert(searchResultProto); + SearchResults searchResults = + new SearchResults(searchResultList, searchResultProto.getNextPageToken()); + callback.complete(AppSearchResult.newSuccessfulResult(searchResults)); } catch (Throwable t) { callback.complete(throwableToFailedResult(t)); } finally { @@ -206,8 +215,8 @@ public class AppSearchManagerService extends SystemService { for (int i = 0; i < uris.size(); i++) { String uri = uris.get(i); try { - impl.remove(databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri); - resultBuilder.setSuccess(uri, /*value= */null); + impl.remove(databaseName, GenericDocument.DEFAULT_NAMESPACE, uri); + resultBuilder.setSuccess(uri, /*result= */null); } catch (Throwable t) { resultBuilder.setResult(uri, throwableToFailedResult(t)); } @@ -237,7 +246,7 @@ public class AppSearchManagerService extends SystemService { String schemaType = schemaTypes.get(i); try { impl.removeByType(databaseName, schemaType); - resultBuilder.setSuccess(schemaType, /*value=*/ null); + resultBuilder.setSuccess(schemaType, /*result=*/ null); } catch (Throwable t) { resultBuilder.setResult(schemaType, throwableToFailedResult(t)); } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverter.java new file mode 100644 index 000000000000..fdeb90dc9b0e --- /dev/null +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverter.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020 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 com.android.server.appsearch.external.localbackend.converter; + +import android.os.Bundle; + +import android.annotation.NonNull; + +import android.app.appsearch.GenericDocument; +import com.android.internal.util.Preconditions; + +import com.google.android.icing.proto.DocumentProto; +import com.google.android.icing.proto.PropertyProto; +import com.google.protobuf.ByteString; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Translates a {@link GenericDocument} into a {@link DocumentProto}. + * @hide + */ + +public final class GenericDocumentToProtoConverter { + private GenericDocumentToProtoConverter() {} + + /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */ + @NonNull + @SuppressWarnings("unchecked") + public static DocumentProto convert(@NonNull GenericDocument document) { + Preconditions.checkNotNull(document); + Bundle properties = document.getBundle().getBundle(GenericDocument.PROPERTIES_FIELD); + DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder(); + mProtoBuilder.setUri(document.getUri()) + .setSchema(document.getSchemaType()) + .setNamespace(document.getNamespace()) + .setScore(document.getScore()) + .setTtlMs(document.getTtlMillis()) + .setCreationTimestampMs(document.getCreationTimestampMillis()); + ArrayList<String> keys = new ArrayList<>(properties.keySet()); + Collections.sort(keys); + for (int i = 0; i < keys.size(); i++) { + String name = keys.get(i); + Object values = properties.get(name); + PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(name); + if (values instanceof boolean[]) { + for (boolean value : (boolean[]) values) { + propertyProto.addBooleanValues(value); + } + } else if (values instanceof long[]) { + for (long value : (long[]) values) { + propertyProto.addInt64Values(value); + } + } else if (values instanceof double[]) { + for (double value : (double[]) values) { + propertyProto.addDoubleValues(value); + } + } else if (values instanceof String[]) { + for (String value : (String[]) values) { + propertyProto.addStringValues(value); + } + } else if (values instanceof ArrayList) { + for (Bundle bundle : (ArrayList<Bundle>) values) { + byte[] value = bundle.getByteArray(GenericDocument.BYTE_ARRAY_FIELD); + propertyProto.addBytesValues(ByteString.copyFrom(value)); + } + } else if (values instanceof Bundle[]) { + for (Bundle bundle : (Bundle[]) values) { + GenericDocument value = new GenericDocument(bundle); + propertyProto.addDocumentValues(convert(value)); + } + } else { + throw new IllegalStateException( + "Property \"" + name + "\" has unsupported value type \"" + + values.getClass().getSimpleName() + "\""); + } + mProtoBuilder.addProperties(propertyProto); + } + return mProtoBuilder.build(); + } + + /** Converts a {@link DocumentProto} into a {@link GenericDocument}. */ + @NonNull + public static GenericDocument convert(@NonNull DocumentProto proto) { + Preconditions.checkNotNull(proto); + GenericDocument.Builder<?> documentBuilder = + new GenericDocument.Builder<>(proto.getUri(), proto.getSchema()) + .setNamespace(proto.getNamespace()) + .setScore(proto.getScore()) + .setTtlMillis(proto.getTtlMs()) + .setCreationTimestampMillis(proto.getCreationTimestampMs()); + + for (int i = 0; i < proto.getPropertiesCount(); i++) { + PropertyProto property = proto.getProperties(i); + String name = property.getName(); + if (property.getBooleanValuesCount() > 0) { + boolean[] values = new boolean[property.getBooleanValuesCount()]; + for (int j = 0; j < values.length; j++) { + values[j] = property.getBooleanValues(j); + } + documentBuilder.setProperty(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); + } + documentBuilder.setProperty(name, 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); + } + documentBuilder.setProperty(name, values); + } else if (property.getStringValuesCount() > 0) { + String[] values = new String[property.getStringValuesCount()]; + for (int j = 0; j < values.length; j++) { + values[j] = property.getStringValues(j); + } + documentBuilder.setProperty(name, 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(); + } + documentBuilder.setProperty(name, values); + } else if (property.getDocumentValuesCount() > 0) { + GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()]; + for (int j = 0; j < values.length; j++) { + values[j] = convert(property.getDocumentValues(j)); + } + documentBuilder.setProperty(name, values); + } else { + throw new IllegalStateException("Unknown type of value: " + name); + } + } + return documentBuilder.build(); + } +} diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchResultToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchResultToProtoConverter.java new file mode 100644 index 000000000000..524c80dd0609 --- /dev/null +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchResultToProtoConverter.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020 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 com.android.server.appsearch.external.localbackend.converter; + +import android.os.Bundle; + +import android.annotation.NonNull; + +import android.app.appsearch.GenericDocument; +import android.app.appsearch.SearchResult; +import android.app.appsearch.SearchResults; + +import com.google.android.icing.proto.SearchResultProto; +import com.google.android.icing.proto.SnippetMatchProto; +import com.google.android.icing.proto.SnippetProto; + +import java.util.ArrayList; +import java.util.List; + +/** + * Translates a {@link SearchResultProto} into {@link SearchResults}. + * @hide + */ + +public class SearchResultToProtoConverter { + private SearchResultToProtoConverter() {} + + /** Translates a {@link SearchResultProto} into a list of {@link SearchResult}. */ + @NonNull + public static List<SearchResult> convert(@NonNull SearchResultProto searchResultProto) { + List<SearchResult> results = new ArrayList<>(searchResultProto.getResultsCount()); + for (int i = 0; i < searchResultProto.getResultsCount(); i++) { + results.add(convertSearchResult(searchResultProto.getResults(i))); + } + return results; + } + + /** Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. */ + @NonNull + static SearchResult convertSearchResult(@NonNull SearchResultProto.ResultProto proto) { + Bundle bundle = new Bundle(); + GenericDocument document = GenericDocumentToProtoConverter.convert(proto.getDocument()); + bundle.putBundle(SearchResult.DOCUMENT_FIELD, document.getBundle()); + + ArrayList<Bundle> matchList = null; + if (proto.hasSnippet()) { + matchList = new ArrayList<>(); + for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) { + SnippetProto.EntryProto entry = proto.getSnippet().getEntries(i); + for (int j = 0; j < entry.getSnippetMatchesCount(); j++) { + Bundle matchInfoBundle = convertToMatchInfoBundle( + entry.getSnippetMatches(j), entry.getPropertyName()); + matchList.add(matchInfoBundle); + } + } + } + bundle.putParcelableArrayList(SearchResult.MATCHES_FIELD, matchList); + + return new SearchResult(bundle); + } + + private static Bundle convertToMatchInfoBundle( + SnippetMatchProto snippetMatchProto, String propertyPath) { + Bundle bundle = new Bundle(); + bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, propertyPath); + bundle.putInt( + SearchResult.MatchInfo.VALUES_INDEX_FIELD, snippetMatchProto.getValuesIndex()); + bundle.putInt( + SearchResult.MatchInfo.EXACT_MATCH_POSITION_LOWER_FIELD, + snippetMatchProto.getExactMatchPosition()); + bundle.putInt( + SearchResult.MatchInfo.EXACT_MATCH_POSITION_UPPER_FIELD, + snippetMatchProto.getExactMatchPosition() + snippetMatchProto.getExactMatchBytes()); + bundle.putInt( + SearchResult.MatchInfo.WINDOW_POSITION_LOWER_FIELD, + snippetMatchProto.getWindowPosition()); + bundle.putInt( + SearchResult.MatchInfo.WINDOW_POSITION_UPPER_FIELD, + snippetMatchProto.getWindowPosition() + snippetMatchProto.getWindowBytes()); + return bundle; + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java b/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java deleted file mode 100644 index ac0f44bb17e4..000000000000 --- a/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2020 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; - -import static com.google.common.truth.Truth.assertThat; - -import static org.testng.Assert.assertThrows; - -import android.app.appsearch.proto.DocumentProto; -import android.app.appsearch.proto.SearchResultProto; - -import androidx.test.filters.SmallTest; - -import org.junit.Test; - -@SmallTest -public class SearchResultsTest { - - @Test - public void testSearchResultsEqual() { - final String uri = "testUri"; - final String schemaType = "testSchema"; - SearchResultProto.ResultProto result1 = SearchResultProto.ResultProto.newBuilder() - .setDocument(DocumentProto.newBuilder() - .setUri(uri) - .setSchema(schemaType) - .build()) - .build(); - SearchResultProto searchResults1 = SearchResultProto.newBuilder() - .addResults(result1) - .build(); - SearchResults res1 = new SearchResults(searchResults1); - SearchResultProto.ResultProto result2 = SearchResultProto.ResultProto.newBuilder() - .setDocument(DocumentProto.newBuilder() - .setUri(uri) - .setSchema(schemaType) - .build()) - .build(); - SearchResultProto searchResults2 = SearchResultProto.newBuilder() - .addResults(result2) - .build(); - SearchResults res2 = new SearchResults(searchResults2); - assertThat(res1.toString()).isEqualTo(res2.toString()); - } - - @Test - public void buildSearchSpecWithoutTermMatchType() { - assertThrows(RuntimeException.class, () -> new SearchSpec.Builder() - .setSchemaTypes("testSchemaType") - .build()); - } -} diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchEmailTest.java index 6aa16cc1e323..ac2d4b5c5f7d 100644 --- a/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java +++ b/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchEmailTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2020 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. @@ -18,11 +18,8 @@ package android.app.appsearch; import static com.google.common.truth.Truth.assertThat; -import androidx.test.filters.SmallTest; - import org.junit.Test; -@SmallTest public class AppSearchEmailTest { @Test diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java index 54a281f2a931..1f2c12bca028 100644 --- a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java +++ b/core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2020 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. @@ -18,36 +18,25 @@ package android.app.appsearch; import static com.google.common.truth.Truth.assertThat; -import static org.testng.Assert.assertThrows; - -import android.app.appsearch.proto.DocumentProto; -import android.app.appsearch.proto.PropertyProto; -import android.app.appsearch.protobuf.ByteString; - -import androidx.test.filters.SmallTest; - +import static org.testng.Assert.expectThrows; import org.junit.Test; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -@SmallTest -public class AppSearchDocumentTest { +public class GenericDocumentTest { 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}; - private static final AppSearchDocument sDocumentProperties1 = new AppSearchDocument + private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7}; + private static final GenericDocument sDocumentProperties1 = new GenericDocument .Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1") + .setCreationTimestampMillis(12345L) .build(); - private static final AppSearchDocument sDocumentProperties2 = new AppSearchDocument + private static final GenericDocument sDocumentProperties2 = new GenericDocument .Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2") + .setCreationTimestampMillis(6789L) .build(); @Test public void testDocumentEquals_Identical() { - AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setTtlMillis(1L) .setProperty("longKey1", 1L, 2L, 3L) @@ -57,7 +46,7 @@ public class AppSearchDocumentTest { .setProperty("byteKey1", sByteArray1, sByteArray2) .setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2) .build(); - AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setTtlMillis(1L) .setProperty("longKey1", 1L, 2L, 3L) @@ -73,7 +62,7 @@ public class AppSearchDocumentTest { @Test public void testDocumentEquals_DifferentOrder() { - AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setProperty("longKey1", 1L, 2L, 3L) .setProperty("byteKey1", sByteArray1, sByteArray2) @@ -84,7 +73,7 @@ public class AppSearchDocumentTest { .build(); // Create second document with same parameter but different order. - AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setProperty("booleanKey1", true, false, true) .setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2) @@ -99,13 +88,13 @@ public class AppSearchDocumentTest { @Test public void testDocumentEquals_Failure() { - AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setProperty("longKey1", 1L, 2L, 3L) .build(); // Create second document with same order but different value. - AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setProperty("longKey1", 1L, 2L, 4L) // Different .build(); @@ -115,13 +104,13 @@ public class AppSearchDocumentTest { @Test public void testDocumentEquals_Failure_RepeatedFieldOrder() { - AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setProperty("booleanKey1", true, false, true) .build(); // Create second document with same order but different value. - AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setProperty("booleanKey1", true, true, false) // Different .build(); @@ -131,11 +120,10 @@ public class AppSearchDocumentTest { @Test public void testDocumentGetSingleValue() { - AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setScore(1) .setTtlMillis(1L) - .setScore(1) .setProperty("longKey1", 1L) .setProperty("doubleKey1", 1.0) .setProperty("booleanKey1", true) @@ -159,7 +147,7 @@ public class AppSearchDocumentTest { @Test public void testDocumentGetArrayValues() { - AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") .setCreationTimestampMillis(5L) .setProperty("longKey1", 1L, 2L, 3L) .setProperty("doubleKey1", 1.0, 2.0, 3.0) @@ -185,8 +173,51 @@ public class AppSearchDocumentTest { } @Test + public void testDocument_ToString() throws Exception { + GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") + .setCreationTimestampMillis(5L) + .setProperty("longKey1", 1L, 2L, 3L) + .setProperty("doubleKey1", 1.0, 2.0, 3.0) + .setProperty("booleanKey1", true, false, true) + .setProperty("stringKey1", "String1", "String2", "String3") + .setProperty("byteKey1", sByteArray1, sByteArray2) + .setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2) + .build(); + String exceptedString = "{ key: 'creationTimestampMillis' value: 5 } " + + "{ key: 'namespace' value: } " + + "{ key: 'properties' value: " + + "{ key: 'booleanKey1' value: [ 'true' 'false' 'true' ] } " + + "{ key: 'byteKey1' value: " + + "{ key: 'byteArray' value: [ '1' '2' '3' ] } " + + "{ key: 'byteArray' value: [ '4' '5' '6' '7' ] } } " + + "{ key: 'documentKey1' value: [ '" + + "{ key: 'creationTimestampMillis' value: 12345 } " + + "{ key: 'namespace' value: } " + + "{ key: 'properties' value: } " + + "{ key: 'schemaType' value: sDocumentPropertiesSchemaType1 } " + + "{ key: 'score' value: 0 } " + + "{ key: 'ttlMillis' value: 0 } " + + "{ key: 'uri' value: sDocumentProperties1 } ' '" + + "{ key: 'creationTimestampMillis' value: 6789 } " + + "{ key: 'namespace' value: } " + + "{ key: 'properties' value: } " + + "{ key: 'schemaType' value: sDocumentPropertiesSchemaType2 } " + + "{ key: 'score' value: 0 } " + + "{ key: 'ttlMillis' value: 0 } " + + "{ key: 'uri' value: sDocumentProperties2 } ' ] } " + + "{ key: 'doubleKey1' value: [ '1.0' '2.0' '3.0' ] } " + + "{ key: 'longKey1' value: [ '1' '2' '3' ] } " + + "{ key: 'stringKey1' value: [ 'String1' 'String2' 'String3' ] } } " + + "{ key: 'schemaType' value: schemaType1 } " + + "{ key: 'score' value: 0 } " + + "{ key: 'ttlMillis' value: 0 } " + + "{ key: 'uri' value: uri1 } "; + assertThat(document.toString()).isEqualTo(exceptedString); + } + + @Test public void testDocumentGetValues_DifferentTypes() { - AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1") + GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1") .setScore(1) .setProperty("longKey1", 1L) .setProperty("booleanKey1", true, false, true) @@ -213,53 +244,8 @@ public class AppSearchDocumentTest { @Test public void testDocumentInvalid() { - AppSearchDocument.Builder builder = new AppSearchDocument.Builder("uri1", "schemaType1"); - assertThrows( + GenericDocument.Builder builder = new GenericDocument.Builder("uri1", "schemaType1"); + expectThrows( IllegalArgumentException.class, () -> builder.setProperty("test", new boolean[]{})); } - - @Test - public void testDocumentProtoPopulation() { - 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) - .setProperty("documentKey1", sDocumentProperties1) - .build(); - - // Create the Document proto. Need to sort the property order by key. - DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder() - .setUri("uri1") - .setSchema("schemaType1") - .setCreationTimestampMs(5L) - .setScore(1) - .setTtlMs(1L) - .setNamespace(""); - HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>(); - propertyProtoMap.put("longKey1", - PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L)); - propertyProtoMap.put("doubleKey1", - PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0)); - propertyProtoMap.put("booleanKey1", - 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))); - propertyProtoMap.put("documentKey1", - PropertyProto.newBuilder().setName("documentKey1") - .addDocumentValues(sDocumentProperties1.getProto())); - List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet()); - Collections.sort(sortedKey); - for (int i = 0; i < sortedKey.size(); i++) { - documentProtoBuilder.addProperties(propertyProtoMap.get(sortedKey.get(i))); - } - assertThat(document.getProto()).isEqualTo(documentProtoBuilder.build()); - } } diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java new file mode 100644 index 000000000000..acbf11ae8669 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020 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; + +import static org.testng.Assert.expectThrows; + +import org.junit.Test; + +public class SearchResultsTest { + @Test + public void buildSearchSpecWithoutTermMatchType() { + expectThrows(RuntimeException.class, () -> new SearchSpec.Builder() + .setSchemaTypes("testSchemaType") + .build()); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java index b29483c2e3b3..2c7c35feface 100644 --- a/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java +++ b/core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2020 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. @@ -14,33 +14,30 @@ * limitations under the License. */ -package android.app.appsearch.impl; - -import static com.google.common.truth.Truth.assertThat; +package android.app.appsearch.customer; import android.annotation.NonNull; -import android.app.appsearch.AppSearchDocument; +import android.app.appsearch.GenericDocument; -import androidx.test.filters.SmallTest; +import static com.google.common.truth.Truth.assertThat; import org.junit.Test; /** - * Tests that {@link AppSearchDocument} and {@link AppSearchDocument.Builder} are extendable by + * Tests that {@link GenericDocument} and {@link GenericDocument.Builder} are extendable by * developers. * - * <p>This class is intentionally in a different package than {@link AppSearchDocument} to make sure + * <p>This class is intentionally in a different package than {@link GenericDocument} to make sure * there are no package-private methods required for external developers to add custom types. */ -@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}; - private static AppSearchDocument sDocumentProperties1 = new AppSearchDocument + private static GenericDocument sDocumentProperties1 = new GenericDocument .Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1") .build(); - private static AppSearchDocument sDocumentProperties2 = new AppSearchDocument + private static GenericDocument sDocumentProperties2 = new GenericDocument .Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2") .build(); @@ -77,19 +74,21 @@ public class CustomerDocumentTest { /** * An example document type for test purposes, defined outside of - * {@link android.app.appsearch.AppSearch} (the way an external developer would define it). + * {@link GenericDocument} (the way an external developer would define + * it). */ - private static class CustomerDocument extends AppSearchDocument { - private CustomerDocument(AppSearchDocument document) { + private static class CustomerDocument extends GenericDocument { + private CustomerDocument(GenericDocument document) { super(document); } - public static class Builder extends AppSearchDocument.Builder<CustomerDocument.Builder> { + public static class Builder extends GenericDocument.Builder<CustomerDocument.Builder> { private Builder(@NonNull String uri) { super(uri, "customerDocument"); } @Override + @NonNull public CustomerDocument build() { return new CustomerDocument(super.build()); } diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverterTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverterTest.java new file mode 100644 index 000000000000..a95290d89b59 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverterTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2020 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 com.android.server.appsearch.external.localbackend.converter; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.appsearch.GenericDocument; + +import com.android.server.appsearch.proto.DocumentProto; +import com.android.server.appsearch.proto.PropertyProto; +import com.android.server.appsearch.protobuf.ByteString; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class GenericDocumentToProtoConverterTest { + private static final byte[] BYTE_ARRAY_1 = new byte[]{(byte) 1, (byte) 2, (byte) 3}; + private static final byte[] BYTE_ARRAY_2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7}; + private static final GenericDocument DOCUMENT_PROPERTIES_1 = + new GenericDocument.Builder<GenericDocument.Builder<?>>( + "sDocumentProperties1", "sDocumentPropertiesSchemaType1") + .setCreationTimestampMillis(12345L) + .build(); + private static final GenericDocument DOCUMENT_PROPERTIES_2 = + new GenericDocument.Builder<GenericDocument.Builder<?>>( + "sDocumentProperties2", "sDocumentPropertiesSchemaType2") + .setCreationTimestampMillis(6789L) + .build(); + + @Test + public void testDocumentProtoConvert() { + GenericDocument document = + new GenericDocument.Builder<GenericDocument.Builder<?>>("uri1", "schemaType1") + .setCreationTimestampMillis(5L) + .setScore(1) + .setTtlMillis(1L) + .setNamespace("namespace") + .setProperty("longKey1", 1L) + .setProperty("doubleKey1", 1.0) + .setProperty("booleanKey1", true) + .setProperty("stringKey1", "test-value1") + .setProperty("byteKey1", BYTE_ARRAY_1, BYTE_ARRAY_2) + .setProperty("documentKey1", DOCUMENT_PROPERTIES_1) + .setProperty(GenericDocument.PROPERTIES_FIELD, DOCUMENT_PROPERTIES_2) + .build(); + + // Create the Document proto. Need to sort the property order by key. + DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder() + .setUri("uri1") + .setSchema("schemaType1") + .setCreationTimestampMs(5L) + .setScore(1) + .setTtlMs(1L) + .setNamespace("namespace"); + HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>(); + propertyProtoMap.put("longKey1", + PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L)); + propertyProtoMap.put("doubleKey1", + PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0)); + propertyProtoMap.put("booleanKey1", + 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(BYTE_ARRAY_1)) + .addBytesValues(ByteString.copyFrom(BYTE_ARRAY_2))); + propertyProtoMap.put("documentKey1", + PropertyProto.newBuilder().setName("documentKey1") + .addDocumentValues( + GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_1))); + propertyProtoMap.put(GenericDocument.PROPERTIES_FIELD, + PropertyProto.newBuilder().setName(GenericDocument.PROPERTIES_FIELD) + .addDocumentValues( + GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_2))); + List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet()); + Collections.sort(sortedKey); + for (String key : sortedKey) { + documentProtoBuilder.addProperties(propertyProtoMap.get(key)); + } + DocumentProto documentProto = documentProtoBuilder.build(); + assertThat(GenericDocumentToProtoConverter.convert(document)) + .isEqualTo(documentProto); + assertThat(document).isEqualTo(GenericDocumentToProtoConverter.convert(documentProto)); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/SnippetTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SnippetTest.java index 95f5b105402c..e9357aa60632 100644 --- a/core/tests/coretests/src/android/app/appsearch/SnippetTest.java +++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SnippetTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright 2020 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. @@ -14,24 +14,23 @@ * limitations under the License. */ -package android.app.appsearch; +package com.android.server.appsearch.external.localbackend.converter; import static com.google.common.truth.Truth.assertThat; -import android.app.appsearch.proto.DocumentProto; -import android.app.appsearch.proto.PropertyProto; -import android.app.appsearch.proto.SearchResultProto; -import android.app.appsearch.proto.SnippetMatchProto; -import android.app.appsearch.proto.SnippetProto; +import android.app.appsearch.SearchResult; -import androidx.test.filters.SmallTest; +import com.android.server.appsearch.proto.DocumentProto; +import com.android.server.appsearch.proto.PropertyProto; +import com.android.server.appsearch.proto.SearchResultProto; +import com.android.server.appsearch.proto.SnippetMatchProto; +import com.android.server.appsearch.proto.SnippetProto; import org.junit.Test; -@SmallTest public class SnippetTest { - // TODO(sidchhabra): Add tests for Double and Long Snippets. + // TODO(tytytyww): Add tests for Double and Long Snippets. @Test public void testSingleStringSnippet() { @@ -74,22 +73,26 @@ public class SnippetTest { SearchResultProto searchResultProto = SearchResultProto.newBuilder() .addResults(resultProto) .build(); - SearchResults searchResults = new SearchResults(searchResultProto); // Making ResultReader and getting Snippet values. - while (searchResults.hasNext()) { - SearchResults.Result result = searchResults.next(); - MatchInfo match = result.getMatchInfo().get(0); + for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) { + SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto); + SearchResult.MatchInfo match = result.getMatches().get(0); assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString); assertThat(match.getFullText()).isEqualTo(propertyValueString); assertThat(match.getExactMatch()).isEqualTo(exactMatch); + assertThat(match.getExactMatchPosition()).isEqualTo( + new SearchResult.MatchRange(/*lower=*/29, /*upper=*/32)); + assertThat(match.getFullText()).isEqualTo(propertyValueString); + assertThat(match.getSnippetPosition()).isEqualTo( + new SearchResult.MatchRange(/*lower=*/26, /*upper=*/32)); assertThat(match.getSnippet()).isEqualTo(window); } } - // TODO(sidchhabra): Add tests for Double and Long Snippets. + // TODO(tytytyww): Add tests for Double and Long Snippets. @Test - public void testNoSnippets() { + public void testNoSnippets() throws Exception { final String propertyKeyString = "content"; final String propertyValueString = "A commonly used fake word is foo.\n" @@ -117,16 +120,15 @@ public class SnippetTest { SearchResultProto searchResultProto = SearchResultProto.newBuilder() .addResults(resultProto) .build(); - SearchResults searchResults = new SearchResults(searchResultProto); - while (searchResults.hasNext()) { - SearchResults.Result result = searchResults.next(); - assertThat(result.getMatchInfo()).isEqualTo(null); + for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) { + SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto); + assertThat(result.getMatches()).isEqualTo(null); } } @Test - public void testMultipleStringSnippet() { + public void testMultipleStringSnippet() throws Exception { final String searchWord = "Test"; // Building the SearchResult received from query. @@ -178,22 +180,29 @@ public class SnippetTest { SearchResultProto searchResultProto = SearchResultProto.newBuilder() .addResults(resultProto) .build(); - SearchResults searchResults = new SearchResults(searchResultProto); // Making ResultReader and getting Snippet values. - while (searchResults.hasNext()) { - SearchResults.Result result = searchResults.next(); + for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) { + SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto); - MatchInfo match1 = result.getMatchInfo().get(0); + SearchResult.MatchInfo match1 = result.getMatches().get(0); assertThat(match1.getPropertyPath()).isEqualTo("sender.name"); assertThat(match1.getFullText()).isEqualTo("Test Name Jr."); + assertThat(match1.getExactMatchPosition()).isEqualTo( + new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4)); assertThat(match1.getExactMatch()).isEqualTo("Test"); + assertThat(match1.getSnippetPosition()).isEqualTo( + new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9)); assertThat(match1.getSnippet()).isEqualTo("Test Name"); - MatchInfo match2 = result.getMatchInfo().get(1); + SearchResult.MatchInfo match2 = result.getMatches().get(1); assertThat(match2.getPropertyPath()).isEqualTo("sender.email"); assertThat(match2.getFullText()).isEqualTo("TestNameJr@gmail.com"); + assertThat(match2.getExactMatchPosition()).isEqualTo( + new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20)); assertThat(match2.getExactMatch()).isEqualTo("TestNameJr@gmail.com"); + assertThat(match2.getSnippetPosition()).isEqualTo( + new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20)); assertThat(match2.getSnippet()).isEqualTo("TestNameJr@gmail.com"); } } |