Merge GenericDocument and SearchResult work from Jetpack.
Bug: 162450968
Test: AppSearchManagerTest
Change-Id: I5617ddad49fa27c6e45fc9742aa27607f0cb2413
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 7d2b64e..0000000
--- 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 b13dd9f..5f2fabe 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 @@
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 @@
/**
* 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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
*
* @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 @@
* 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 @@
/**
* 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 @@
/**
* Set the from address of {@link AppSearchEmail}
- * @hide
*/
@NonNull
public AppSearchEmail.Builder setFrom(@NonNull String from) {
@@ -194,7 +187,6 @@
/**
* Set the destination address of {@link AppSearchEmail}
- * @hide
*/
@NonNull
public AppSearchEmail.Builder setTo(@NonNull String... to) {
@@ -204,7 +196,6 @@
/**
* Set the CC list of {@link AppSearchEmail}
- * @hide
*/
@NonNull
public AppSearchEmail.Builder setCc(@NonNull String... cc) {
@@ -214,7 +205,6 @@
/**
* Set the BCC list of {@link AppSearchEmail}
- * @hide
*/
@NonNull
public AppSearchEmail.Builder setBcc(@NonNull String... bcc) {
@@ -224,7 +214,6 @@
/**
* Set the subject of {@link AppSearchEmail}
- * @hide
*/
@NonNull
public AppSearchEmail.Builder setSubject(@NonNull String subject) {
@@ -234,7 +223,6 @@
/**
* Set the body of {@link AppSearchEmail}
- * @hide
*/
@NonNull
public AppSearchEmail.Builder setBody(@NonNull String body) {
@@ -242,11 +230,7 @@
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 ad51a5c..b38bb05 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 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 @@
* <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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
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;
+ for (Map.Entry<String, Bundle> bundleEntry : bundleResult.getSuccesses().entrySet()) {
+ GenericDocument document;
try {
- documentProto = DocumentProto.parseFrom(protoResult.getValue());
- } catch (InvalidProtocolBufferException e) {
- documentResultBuilder.setFailure(
- protoResult.getKey(), AppSearchResult.RESULT_IO_ERROR, e.getMessage());
- continue;
- }
- AppSearchDocument 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 @@
* @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 0000000..9fe2c67
--- /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 c710a29..8e18346 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 @@
parcelable AppSearchResult;
parcelable AppSearchBatchResult;
+parcelable SearchResults;
/** {@hide} */
interface IAppSearchManager {
@@ -41,7 +42,7 @@
/**
* 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 @@
* {@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 @@
*
* @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 5ce2960..0000000
--- 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 0000000..758280b
--- /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 7287fe6..9f37625 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> {
+public final class SearchResults implements Parcelable {
+ final List<SearchResult> mResults;
+ final long mNextPageToken;
- private final SearchResultProto mSearchResultProto;
- private int mNextIdx;
+ public SearchResults(@NonNull List<SearchResult> results, long nextPageToken) {
+ mResults = results;
+ mNextPageToken = nextPageToken;
+ }
- /** @hide */
- public SearchResults(SearchResultProto searchResultProto) {
- mSearchResultProto = searchResultProto;
+ 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();
}
@Override
- public boolean hasNext() {
- return mNextIdx < mSearchResultProto.getResultsCount();
+ 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());
+ }
+ dest.writeList(resultBundles);
+ dest.writeLong(mNextPageToken);
}
- @NonNull
@Override
- public Result next() {
- if (!hasNext()) {
- throw new NoSuchElementException();
- }
- Result result = new Result(mSearchResultProto.getResults(mNextIdx));
- mNextIdx++;
- return result;
+ 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 817c3ef..c871905 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 @@
/**
* 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 @@
/**
* 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 4bc0c39..06612ac 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 @@
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.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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 0000000..fdeb90d
--- /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 0000000..524c80d
--- /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 ac0f44b..0000000
--- 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
similarity index 94%
rename from core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java
rename to core/tests/coretests/src/android/app/appsearch/external/app/AppSearchEmailTest.java
index 6aa16cc..ac2d4b5 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 @@
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
similarity index 69%
rename from core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
rename to core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java
index 54a281f2..1f2c12b 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 @@
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 @@
.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 @@
@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 @@
.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 @@
@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 @@
@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 @@
@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 @@
@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 @@
}
@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 @@
@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 0000000..acbf11a
--- /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
similarity index 80%
rename from core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java
rename to core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java
index b29483c..2c7c35f 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 @@
/**
* 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 0000000..a95290d
--- /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
similarity index 75%
rename from core/tests/coretests/src/android/app/appsearch/SnippetTest.java
rename to services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SnippetTest.java
index 95f5b10..e9357aa 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 @@
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 @@
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 @@
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");
}
}