Merge "Merge GenericDocument and SearchResult work from Jetpack."

This commit is contained in:
Alexander Dorokhine
2020-10-17 01:11:05 +00:00
committed by Android (Google) Code Review
19 changed files with 1926 additions and 1288 deletions

View File

@@ -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);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,26 @@
package android.app.appsearch;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.appsearch.AppSearchSchema.PropertyConfig;
/**
* Encapsulates a {@link AppSearchDocument} that represent an email.
* Encapsulates a {@link GenericDocument} that represent an email.
*
* <p>This class is a higher level implement of {@link AppSearchDocument}.
* <p>This class is a higher level implement of {@link GenericDocument}.
*
* <p>This class will eventually migrate to Jetpack, where it will become public API.
*
* @hide
*/
public class AppSearchEmail extends AppSearchDocument {
public class AppSearchEmail extends GenericDocument {
/** The name of the schema type for {@link AppSearchEmail} documents.*/
public static final String SCHEMA_TYPE = "builtin:Email";
private static final String KEY_FROM = "from";
private static final String KEY_TO = "to";
private static final String KEY_CC = "cc";
@@ -37,46 +43,43 @@ public class AppSearchEmail extends AppSearchDocument {
private static final String KEY_SUBJECT = "subject";
private static final String KEY_BODY = "body";
/** The name of the schema type for {@link AppSearchEmail} documents.*/
public static final String SCHEMA_TYPE = "builtin:Email";
public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
.addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_FROM)
.addProperty(new PropertyConfig.Builder(KEY_FROM)
.setDataType(PropertyConfig.DATA_TYPE_STRING)
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_TO)
).addProperty(new PropertyConfig.Builder(KEY_TO)
.setDataType(PropertyConfig.DATA_TYPE_STRING)
.setCardinality(PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_CC)
).addProperty(new PropertyConfig.Builder(KEY_CC)
.setDataType(PropertyConfig.DATA_TYPE_STRING)
.setCardinality(PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BCC)
).addProperty(new PropertyConfig.Builder(KEY_BCC)
.setDataType(PropertyConfig.DATA_TYPE_STRING)
.setCardinality(PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_SUBJECT)
).addProperty(new PropertyConfig.Builder(KEY_SUBJECT)
.setDataType(PropertyConfig.DATA_TYPE_STRING)
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BODY)
).addProperty(new PropertyConfig.Builder(KEY_BODY)
.setDataType(PropertyConfig.DATA_TYPE_STRING)
.setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
@@ -87,12 +90,11 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* Creates a new {@link AppSearchEmail} from the contents of an existing
* {@link AppSearchDocument}.
* {@link GenericDocument}.
*
* @param document The {@link AppSearchDocument} containing the email content.
* @hide
* @param document The {@link GenericDocument} containing the email content.
*/
public AppSearchEmail(@NonNull AppSearchDocument document) {
public AppSearchEmail(@NonNull GenericDocument document) {
super(document);
}
@@ -101,7 +103,6 @@ public class AppSearchEmail extends AppSearchDocument {
*
* @return Returns the subject of {@link AppSearchEmail} or {@code null} if it's not been set
* yet.
* @hide
*/
@Nullable
public String getFrom() {
@@ -113,7 +114,6 @@ public class AppSearchEmail extends AppSearchDocument {
*
* @return Returns the destination addresses of {@link AppSearchEmail} or {@code null} if it's
* not been set yet.
* @hide
*/
@Nullable
public String[] getTo() {
@@ -125,7 +125,6 @@ public class AppSearchEmail extends AppSearchDocument {
*
* @return Returns the CC list of {@link AppSearchEmail} or {@code null} if it's not been set
* yet.
* @hide
*/
@Nullable
public String[] getCc() {
@@ -137,7 +136,6 @@ public class AppSearchEmail extends AppSearchDocument {
*
* @return Returns the BCC list of {@link AppSearchEmail} or {@code null} if it's not been set
* yet.
* @hide
*/
@Nullable
public String[] getBcc() {
@@ -149,7 +147,6 @@ public class AppSearchEmail extends AppSearchDocument {
*
* @return Returns the value subject of {@link AppSearchEmail} or {@code null} if it's not been
* set yet.
* @hide
*/
@Nullable
public String getSubject() {
@@ -160,7 +157,6 @@ public class AppSearchEmail extends AppSearchDocument {
* Get the body of {@link AppSearchEmail}.
*
* @return Returns the body of {@link AppSearchEmail} or {@code null} if it's not been set yet.
* @hide
*/
@Nullable
public String getBody() {
@@ -169,14 +165,12 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* The builder class for {@link AppSearchEmail}.
* @hide
*/
public static class Builder extends AppSearchDocument.Builder<AppSearchEmail.Builder> {
public static class Builder extends GenericDocument.Builder<AppSearchEmail.Builder> {
/**
* Create a new {@link AppSearchEmail.Builder}
* @param uri The Uri of the Email.
* @hide
*/
public Builder(@NonNull String uri) {
super(uri, SCHEMA_TYPE);
@@ -184,7 +178,6 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* Set the from address of {@link AppSearchEmail}
* @hide
*/
@NonNull
public AppSearchEmail.Builder setFrom(@NonNull String from) {
@@ -194,7 +187,6 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* Set the destination address of {@link AppSearchEmail}
* @hide
*/
@NonNull
public AppSearchEmail.Builder setTo(@NonNull String... to) {
@@ -204,7 +196,6 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* Set the CC list of {@link AppSearchEmail}
* @hide
*/
@NonNull
public AppSearchEmail.Builder setCc(@NonNull String... cc) {
@@ -214,7 +205,6 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* Set the BCC list of {@link AppSearchEmail}
* @hide
*/
@NonNull
public AppSearchEmail.Builder setBcc(@NonNull String... bcc) {
@@ -224,7 +214,6 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* Set the subject of {@link AppSearchEmail}
* @hide
*/
@NonNull
public AppSearchEmail.Builder setSubject(@NonNull String subject) {
@@ -234,7 +223,6 @@ public class AppSearchEmail extends AppSearchDocument {
/**
* Set the body of {@link AppSearchEmail}
* @hide
*/
@NonNull
public AppSearchEmail.Builder setBody(@NonNull String body) {
@@ -242,11 +230,7 @@ public class AppSearchEmail extends AppSearchDocument {
return this;
}
/**
* Builds the {@link AppSearchEmail} object.
*
* @hide
*/
/** Builds the {@link AppSearchEmail} object. */
@NonNull
@Override
public AppSearchEmail build() {

View File

@@ -23,11 +23,6 @@ import android.os.RemoteException;
import com.android.internal.infra.AndroidFuture;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.SearchResultProto;
import com.google.android.icing.proto.StatusProto;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -78,8 +73,8 @@ public class AppSearchManager {
* <li>Removal of an existing type
* <li>Removal of a property from a type
* <li>Changing the data type ({@code boolean}, {@code long}, etc.) of an existing property
* <li>For properties of {@code AppSearchDocument} type, changing the schema type of
* {@code AppSearchDocument}s of that property
* <li>For properties of {@code GenericDocument} type, changing the schema type of
* {@code GenericDocument}s of that property
* <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an
* {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
* OPTIONAL} property into a
@@ -148,31 +143,31 @@ public class AppSearchManager {
}
/**
* Index {@link AppSearchDocument}s into AppSearch.
* Index {@link GenericDocument}s into AppSearch.
*
* <p>You should not call this method directly; instead, use the
* {@code AppSearch#putDocuments()} API provided by JetPack.
*
* <p>Each {@link AppSearchDocument}'s {@code schemaType} field must be set to the name of a
* <p>Each {@link GenericDocument}'s {@code schemaType} field must be set to the name of a
* schema type previously registered via the {@link #setSchema} method.
*
* @param documents {@link AppSearchDocument}s that need to be indexed.
* @param documents {@link GenericDocument}s that need to be indexed.
* @return An {@link AppSearchBatchResult} mapping the document URIs to {@link Void} if they
* were successfully indexed, or a {@link Throwable} describing the failure if they could
* not be indexed.
* @hide
*/
public AppSearchBatchResult<String, Void> putDocuments(
@NonNull List<AppSearchDocument> documents) {
@NonNull List<GenericDocument> documents) {
// TODO(b/146386470): Transmit these documents as a RemoteStream instead of sending them in
// one big list.
List<byte[]> documentsBytes = new ArrayList<>(documents.size());
for (AppSearchDocument document : documents) {
documentsBytes.add(document.getProto().toByteArray());
List<Bundle> documentBundles = new ArrayList<>(documents.size());
for (GenericDocument document : documents) {
documentBundles.add(document.getBundle());
}
AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
try {
mService.putDocuments(documentsBytes, future);
mService.putDocuments(documentBundles, future);
} catch (RemoteException e) {
future.completeExceptionally(e);
}
@@ -180,19 +175,18 @@ public class AppSearchManager {
}
/**
* Retrieves {@link AppSearchDocument}s by URI.
* Retrieves {@link GenericDocument}s by URI.
*
* <p>You should not call this method directly; instead, use the
* {@code AppSearch#getDocuments()} API provided by JetPack.
*
* @param uris URIs of the documents to look up.
* @return An {@link AppSearchBatchResult} mapping the document URIs to
* {@link AppSearchDocument} values if they were successfully retrieved, a {@code null}
* {@link GenericDocument} values if they were successfully retrieved, a {@code null}
* failure if they were not found, or a {@link Throwable} failure describing the problem if
* an error occurred.
*/
public AppSearchBatchResult<String, AppSearchDocument> getDocuments(
@NonNull List<String> uris) {
public AppSearchBatchResult<String, GenericDocument> getDocuments(@NonNull List<String> uris) {
// TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
// them in one big list.
AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
@@ -202,43 +196,35 @@ public class AppSearchManager {
future.completeExceptionally(e);
}
// Deserialize the protos into Document objects
AppSearchBatchResult<String, byte[]> protoResults = getFutureOrThrow(future);
AppSearchBatchResult.Builder<String, AppSearchDocument> documentResultBuilder =
// Translate from document bundles to GenericDocument instances
AppSearchBatchResult<String, Bundle> bundleResult = getFutureOrThrow(future);
AppSearchBatchResult.Builder<String, GenericDocument> documentResultBuilder =
new AppSearchBatchResult.Builder<>();
// Translate successful results
for (Map.Entry<String, byte[]> protoResult : protoResults.getSuccesses().entrySet()) {
DocumentProto documentProto;
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 @@ public class AppSearchManager {
* @hide
*/
@NonNull
public AppSearchResult<SearchResults> query(
public AppSearchResult<List<SearchResult>> query(
@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
// TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
// them in one big list.
AndroidFuture<AppSearchResult> searchResultFuture = new AndroidFuture<>();
AndroidFuture<AppSearchResult> searchResultsFuture = new AndroidFuture<>();
try {
mService.query(queryExpression, searchSpec.getBundle(), searchResultFuture);
mService.query(queryExpression, searchSpec.getBundle(), searchResultsFuture);
} catch (RemoteException e) {
searchResultFuture.completeExceptionally(e);
searchResultsFuture.completeExceptionally(e);
}
// Deserialize the protos into Document objects
AppSearchResult<byte[]> searchResultBytes = getFutureOrThrow(searchResultFuture);
if (!searchResultBytes.isSuccess()) {
// Translate the list of Bundle into a list of SearchResult
AppSearchResult<SearchResults> searchResultsResult = getFutureOrThrow(searchResultsFuture);
if (!searchResultsResult.isSuccess()) {
return AppSearchResult.newFailedResult(
searchResultBytes.getResultCode(), searchResultBytes.getErrorMessage());
searchResultsResult.getResultCode(), searchResultsResult.getErrorMessage());
}
SearchResultProto searchResultProto;
try {
searchResultProto = SearchResultProto.parseFrom(searchResultBytes.getResultValue());
} catch (InvalidProtocolBufferException e) {
return AppSearchResult.newFailedResult(
AppSearchResult.RESULT_INTERNAL_ERROR, e.getMessage());
}
if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
// This should never happen; AppSearchManagerService should catch failed searchResults
// entries and transmit them as a failed AppSearchResult.
return AppSearchResult.newFailedResult(
AppSearchResult.RESULT_INTERNAL_ERROR,
searchResultProto.getStatus().getMessage());
}
return AppSearchResult.newSuccessfulResult(new SearchResults(searchResultProto));
SearchResults searchResults = searchResultsResult.getResultValue();
return AppSearchResult.newSuccessfulResult(searchResults.mResults);
}
/**
* Deletes {@link AppSearchDocument}s by URI.
* Deletes {@link GenericDocument}s by URI.
*
* <p>You should not call this method directly; instead, use the {@code AppSearch#delete()} API
* provided by JetPack.

View File

@@ -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);
}
}
}

View File

@@ -21,6 +21,7 @@ import com.android.internal.infra.AndroidFuture;
parcelable AppSearchResult;
parcelable AppSearchBatchResult;
parcelable SearchResults;
/** {@hide} */
interface IAppSearchManager {
@@ -41,7 +42,7 @@ interface IAppSearchManager {
/**
* Inserts documents into the index.
*
* @param documentsBytes {@link List}&lt;byte[]&gt; of serialized DocumentProtos.
* @param documentBundes List of GenericDocument bundles.
* @param callback
* {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;&gt;.
* If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
@@ -49,18 +50,19 @@ interface IAppSearchManager {
* {@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;
* 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}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link byte[]}&gt;&gt;.
* {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Bundle}&gt;&gt;.
* If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
* {@code callback} will be completed with an
* {@link AppSearchBatchResult}&lt;{@link String}, {@link byte[]}&gt;
* where the keys are document URIs, and the values are serialized Document protos.
* {@link AppSearchBatchResult}&lt;{@link String}, {@link Bundle}&gt;
* where the keys are document URIs, and the values are Document bundles.
*/
void getDocuments(in List<String> uris, in AndroidFuture<AppSearchBatchResult> callback);
@@ -69,8 +71,7 @@ interface IAppSearchManager {
*
* @param queryExpression String to search for
* @param searchSpecBundle SearchSpec bundle
* @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link byte[]}&gt;&gt;
* Will be completed with a serialized {@link SearchResultsProto}.
* @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link SearchResults}&gt;&gt;
*/
void query(
in String queryExpression,

View File

@@ -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 thats 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 thats 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
* thats 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;
}
}

View File

@@ -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 thats 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 thats 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);
}
}
}

View File

@@ -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();
}
};
}

View File

@@ -249,8 +249,10 @@ public final class SearchSpec {
/**
* Only the first {@code snippetCount} documents based on the ranking strategy
* will have snippet information provided.
* <p>If set to 0 (default), snippeting is disabled and
* {@link SearchResults.Result#getMatches} will return {@code null} for that result.
*
* <p>If set to 0 (default), snippeting is disabled and {@link SearchResult#getMatches} will
* return {@code null} for that result.
*
* <p>The value should be set in range[0, 10k].
*/
@NonNull
@@ -264,8 +266,10 @@ public final class SearchSpec {
/**
* Only the first {@code matchesCountPerProperty} matches for a every property of
* {@link GenericDocument} will contain snippet information.
* <p>If set to 0, snippeting is disabled and {@link SearchResults.Result#getMatches}
* will return {@code null} for that result.
*
* <p>If set to 0, snippeting is disabled and {@link SearchResult#getMatches} will return
* {@code null} for that result.
*
* <p>The value should be set in range[0, 10k].
*/
@NonNull

View File

@@ -17,10 +17,12 @@ package com.android.server.appsearch;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchDocument;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.IAppSearchManager;
import android.app.appsearch.SearchResult;
import android.app.appsearch.SearchResults;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.exceptions.AppSearchException;
import android.content.Context;
@@ -32,7 +34,9 @@ import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
import com.android.server.appsearch.external.localbackend.AppSearchImpl;
import com.android.server.appsearch.external.localbackend.converter.GenericDocumentToProtoConverter;
import com.android.server.appsearch.external.localbackend.converter.SchemaToProtoConverter;
import com.android.server.appsearch.external.localbackend.converter.SearchResultToProtoConverter;
import com.android.server.appsearch.external.localbackend.converter.SearchSpecToProtoConverter;
import com.google.android.icing.proto.DocumentProto;
@@ -80,7 +84,7 @@ public class AppSearchManagerService extends SystemService {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
String databaseName = makeDatabaseName(callingUid);
impl.setSchema(databaseName, schemaProtoBuilder.build(), forceOverride);
callback.complete(AppSearchResult.newSuccessfulResult(/*value=*/ null));
callback.complete(AppSearchResult.newSuccessfulResult(/*result=*/ null));
} catch (Throwable t) {
callback.complete(throwableToFailedResult(t));
} finally {
@@ -90,9 +94,9 @@ public class AppSearchManagerService extends SystemService {
@Override
public void putDocuments(
@NonNull List documentsBytes,
@NonNull List<Bundle> documentBundles,
@NonNull AndroidFuture<AppSearchBatchResult> callback) {
Preconditions.checkNotNull(documentsBytes);
Preconditions.checkNotNull(documentBundles);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
int callingUserId = UserHandle.getUserId(callingUid);
@@ -102,12 +106,12 @@ public class AppSearchManagerService extends SystemService {
String databaseName = makeDatabaseName(callingUid);
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < documentsBytes.size(); i++) {
byte[] documentBytes = (byte[]) documentsBytes.get(i);
DocumentProto document = DocumentProto.parseFrom(documentBytes);
for (int i = 0; i < documentBundles.size(); i++) {
GenericDocument document = new GenericDocument(documentBundles.get(i));
DocumentProto documentProto = GenericDocumentToProtoConverter.convert(document);
try {
impl.putDocument(databaseName, document);
resultBuilder.setSuccess(document.getUri(), /*value=*/ null);
impl.putDocument(databaseName, documentProto);
resultBuilder.setSuccess(document.getUri(), /*result=*/ null);
} catch (Throwable t) {
resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
}
@@ -131,18 +135,20 @@ public class AppSearchManagerService extends SystemService {
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
String databaseName = makeDatabaseName(callingUid);
AppSearchBatchResult.Builder<String, byte[]> resultBuilder =
AppSearchBatchResult.Builder<String, Bundle> resultBuilder =
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < uris.size(); i++) {
String uri = uris.get(i);
try {
DocumentProto document = impl.getDocument(
databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri);
if (document == null) {
DocumentProto documentProto = impl.getDocument(
databaseName, GenericDocument.DEFAULT_NAMESPACE, uri);
if (documentProto == null) {
resultBuilder.setFailure(
uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null);
} else {
resultBuilder.setSuccess(uri, document.toByteArray());
GenericDocument genericDocument =
GenericDocumentToProtoConverter.convert(documentProto);
resultBuilder.setSuccess(uri, genericDocument.getBundle());
}
} catch (Throwable t) {
resultBuilder.setResult(uri, throwableToFailedResult(t));
@@ -182,8 +188,11 @@ public class AppSearchManagerService extends SystemService {
searchSpecProto,
SearchSpecToProtoConverter.toResultSpecProto(searchSpec),
SearchSpecToProtoConverter.toScoringSpecProto(searchSpec));
callback.complete(
AppSearchResult.newSuccessfulResult(searchResultProto.toByteArray()));
List<SearchResult> searchResultList =
SearchResultToProtoConverter.convert(searchResultProto);
SearchResults searchResults =
new SearchResults(searchResultList, searchResultProto.getNextPageToken());
callback.complete(AppSearchResult.newSuccessfulResult(searchResults));
} catch (Throwable t) {
callback.complete(throwableToFailedResult(t));
} finally {
@@ -206,8 +215,8 @@ public class AppSearchManagerService extends SystemService {
for (int i = 0; i < uris.size(); i++) {
String uri = uris.get(i);
try {
impl.remove(databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri);
resultBuilder.setSuccess(uri, /*value= */null);
impl.remove(databaseName, GenericDocument.DEFAULT_NAMESPACE, uri);
resultBuilder.setSuccess(uri, /*result= */null);
} catch (Throwable t) {
resultBuilder.setResult(uri, throwableToFailedResult(t));
}
@@ -237,7 +246,7 @@ public class AppSearchManagerService extends SystemService {
String schemaType = schemaTypes.get(i);
try {
impl.removeByType(databaseName, schemaType);
resultBuilder.setSuccess(schemaType, /*value=*/ null);
resultBuilder.setSuccess(schemaType, /*result=*/ null);
} catch (Throwable t) {
resultBuilder.setResult(schemaType, throwableToFailedResult(t));
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,11 +18,8 @@ package android.app.appsearch;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.filters.SmallTest;
import org.junit.Test;
@SmallTest
public class AppSearchEmailTest {
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,36 +18,25 @@ package android.app.appsearch;
import static com.google.common.truth.Truth.assertThat;
import static org.testng.Assert.assertThrows;
import android.app.appsearch.proto.DocumentProto;
import android.app.appsearch.proto.PropertyProto;
import android.app.appsearch.protobuf.ByteString;
import androidx.test.filters.SmallTest;
import static org.testng.Assert.expectThrows;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@SmallTest
public class AppSearchDocumentTest {
public class GenericDocumentTest {
private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};
private static final AppSearchDocument sDocumentProperties1 = new AppSearchDocument
private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
private static final GenericDocument sDocumentProperties1 = new GenericDocument
.Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
.setCreationTimestampMillis(12345L)
.build();
private static final AppSearchDocument sDocumentProperties2 = new AppSearchDocument
private static final GenericDocument sDocumentProperties2 = new GenericDocument
.Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
.setCreationTimestampMillis(6789L)
.build();
@Test
public void testDocumentEquals_Identical() {
AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setTtlMillis(1L)
.setProperty("longKey1", 1L, 2L, 3L)
@@ -57,7 +46,7 @@ public class AppSearchDocumentTest {
.setProperty("byteKey1", sByteArray1, sByteArray2)
.setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2)
.build();
AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setTtlMillis(1L)
.setProperty("longKey1", 1L, 2L, 3L)
@@ -73,7 +62,7 @@ public class AppSearchDocumentTest {
@Test
public void testDocumentEquals_DifferentOrder() {
AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("byteKey1", sByteArray1, sByteArray2)
@@ -84,7 +73,7 @@ public class AppSearchDocumentTest {
.build();
// Create second document with same parameter but different order.
AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("booleanKey1", true, false, true)
.setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2)
@@ -99,13 +88,13 @@ public class AppSearchDocumentTest {
@Test
public void testDocumentEquals_Failure() {
AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.build();
// Create second document with same order but different value.
AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 4L) // Different
.build();
@@ -115,13 +104,13 @@ public class AppSearchDocumentTest {
@Test
public void testDocumentEquals_Failure_RepeatedFieldOrder() {
AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("booleanKey1", true, false, true)
.build();
// Create second document with same order but different value.
AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("booleanKey1", true, true, false) // Different
.build();
@@ -131,11 +120,10 @@ public class AppSearchDocumentTest {
@Test
public void testDocumentGetSingleValue() {
AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setScore(1)
.setTtlMillis(1L)
.setScore(1)
.setProperty("longKey1", 1L)
.setProperty("doubleKey1", 1.0)
.setProperty("booleanKey1", true)
@@ -159,7 +147,7 @@ public class AppSearchDocumentTest {
@Test
public void testDocumentGetArrayValues() {
AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
@@ -184,9 +172,52 @@ public class AppSearchDocumentTest {
.containsExactly(sDocumentProperties1, sDocumentProperties2);
}
@Test
public void testDocument_ToString() throws Exception {
GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "String1", "String2", "String3")
.setProperty("byteKey1", sByteArray1, sByteArray2)
.setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2)
.build();
String exceptedString = "{ key: 'creationTimestampMillis' value: 5 } "
+ "{ key: 'namespace' value: } "
+ "{ key: 'properties' value: "
+ "{ key: 'booleanKey1' value: [ 'true' 'false' 'true' ] } "
+ "{ key: 'byteKey1' value: "
+ "{ key: 'byteArray' value: [ '1' '2' '3' ] } "
+ "{ key: 'byteArray' value: [ '4' '5' '6' '7' ] } } "
+ "{ key: 'documentKey1' value: [ '"
+ "{ key: 'creationTimestampMillis' value: 12345 } "
+ "{ key: 'namespace' value: } "
+ "{ key: 'properties' value: } "
+ "{ key: 'schemaType' value: sDocumentPropertiesSchemaType1 } "
+ "{ key: 'score' value: 0 } "
+ "{ key: 'ttlMillis' value: 0 } "
+ "{ key: 'uri' value: sDocumentProperties1 } ' '"
+ "{ key: 'creationTimestampMillis' value: 6789 } "
+ "{ key: 'namespace' value: } "
+ "{ key: 'properties' value: } "
+ "{ key: 'schemaType' value: sDocumentPropertiesSchemaType2 } "
+ "{ key: 'score' value: 0 } "
+ "{ key: 'ttlMillis' value: 0 } "
+ "{ key: 'uri' value: sDocumentProperties2 } ' ] } "
+ "{ key: 'doubleKey1' value: [ '1.0' '2.0' '3.0' ] } "
+ "{ key: 'longKey1' value: [ '1' '2' '3' ] } "
+ "{ key: 'stringKey1' value: [ 'String1' 'String2' 'String3' ] } } "
+ "{ key: 'schemaType' value: schemaType1 } "
+ "{ key: 'score' value: 0 } "
+ "{ key: 'ttlMillis' value: 0 } "
+ "{ key: 'uri' value: uri1 } ";
assertThat(document.toString()).isEqualTo(exceptedString);
}
@Test
public void testDocumentGetValues_DifferentTypes() {
AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
.setScore(1)
.setProperty("longKey1", 1L)
.setProperty("booleanKey1", true, false, true)
@@ -213,53 +244,8 @@ public class AppSearchDocumentTest {
@Test
public void testDocumentInvalid() {
AppSearchDocument.Builder builder = new AppSearchDocument.Builder("uri1", "schemaType1");
assertThrows(
GenericDocument.Builder builder = new GenericDocument.Builder("uri1", "schemaType1");
expectThrows(
IllegalArgumentException.class, () -> builder.setProperty("test", new boolean[]{}));
}
@Test
public void testDocumentProtoPopulation() {
AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
.setCreationTimestampMillis(5L)
.setScore(1)
.setTtlMillis(1L)
.setProperty("longKey1", 1L)
.setProperty("doubleKey1", 1.0)
.setProperty("booleanKey1", true)
.setProperty("stringKey1", "test-value1")
.setProperty("byteKey1", sByteArray1)
.setProperty("documentKey1", sDocumentProperties1)
.build();
// Create the Document proto. Need to sort the property order by key.
DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
.setUri("uri1")
.setSchema("schemaType1")
.setCreationTimestampMs(5L)
.setScore(1)
.setTtlMs(1L)
.setNamespace("");
HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
propertyProtoMap.put("longKey1",
PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
propertyProtoMap.put("doubleKey1",
PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0));
propertyProtoMap.put("booleanKey1",
PropertyProto.newBuilder().setName("booleanKey1").addBooleanValues(true));
propertyProtoMap.put("stringKey1",
PropertyProto.newBuilder().setName("stringKey1").addStringValues("test-value1"));
propertyProtoMap.put("byteKey1",
PropertyProto.newBuilder().setName("byteKey1").addBytesValues(
ByteString.copyFrom(sByteArray1)));
propertyProtoMap.put("documentKey1",
PropertyProto.newBuilder().setName("documentKey1")
.addDocumentValues(sDocumentProperties1.getProto()));
List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
Collections.sort(sortedKey);
for (int i = 0; i < sortedKey.size(); i++) {
documentProtoBuilder.addProperties(propertyProtoMap.get(sortedKey.get(i)));
}
assertThat(document.getProto()).isEqualTo(documentProtoBuilder.build());
}
}

View File

@@ -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());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,33 +14,30 @@
* limitations under the License.
*/
package android.app.appsearch.impl;
import static com.google.common.truth.Truth.assertThat;
package android.app.appsearch.customer;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchDocument;
import android.app.appsearch.GenericDocument;
import androidx.test.filters.SmallTest;
import static com.google.common.truth.Truth.assertThat;
import org.junit.Test;
/**
* Tests that {@link AppSearchDocument} and {@link AppSearchDocument.Builder} are extendable by
* Tests that {@link GenericDocument} and {@link GenericDocument.Builder} are extendable by
* developers.
*
* <p>This class is intentionally in a different package than {@link AppSearchDocument} to make sure
* <p>This class is intentionally in a different package than {@link GenericDocument} to make sure
* there are no package-private methods required for external developers to add custom types.
*/
@SmallTest
public class CustomerDocumentTest {
private static byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
private static byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};
private static AppSearchDocument sDocumentProperties1 = new AppSearchDocument
private static GenericDocument sDocumentProperties1 = new GenericDocument
.Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
.build();
private static AppSearchDocument sDocumentProperties2 = new AppSearchDocument
private static GenericDocument sDocumentProperties2 = new GenericDocument
.Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
.build();
@@ -77,19 +74,21 @@ public class CustomerDocumentTest {
/**
* An example document type for test purposes, defined outside of
* {@link android.app.appsearch.AppSearch} (the way an external developer would define it).
* {@link GenericDocument} (the way an external developer would define
* it).
*/
private static class CustomerDocument extends AppSearchDocument {
private CustomerDocument(AppSearchDocument document) {
private static class CustomerDocument extends GenericDocument {
private CustomerDocument(GenericDocument document) {
super(document);
}
public static class Builder extends AppSearchDocument.Builder<CustomerDocument.Builder> {
public static class Builder extends GenericDocument.Builder<CustomerDocument.Builder> {
private Builder(@NonNull String uri) {
super(uri, "customerDocument");
}
@Override
@NonNull
public CustomerDocument build() {
return new CustomerDocument(super.build());
}

View File

@@ -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));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 The Android Open Source Project
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,24 +14,23 @@
* limitations under the License.
*/
package android.app.appsearch;
package com.android.server.appsearch.external.localbackend.converter;
import static com.google.common.truth.Truth.assertThat;
import android.app.appsearch.proto.DocumentProto;
import android.app.appsearch.proto.PropertyProto;
import android.app.appsearch.proto.SearchResultProto;
import android.app.appsearch.proto.SnippetMatchProto;
import android.app.appsearch.proto.SnippetProto;
import android.app.appsearch.SearchResult;
import androidx.test.filters.SmallTest;
import com.android.server.appsearch.proto.DocumentProto;
import com.android.server.appsearch.proto.PropertyProto;
import com.android.server.appsearch.proto.SearchResultProto;
import com.android.server.appsearch.proto.SnippetMatchProto;
import com.android.server.appsearch.proto.SnippetProto;
import org.junit.Test;
@SmallTest
public class SnippetTest {
// TODO(sidchhabra): Add tests for Double and Long Snippets.
// TODO(tytytyww): Add tests for Double and Long Snippets.
@Test
public void testSingleStringSnippet() {
@@ -74,22 +73,26 @@ public class SnippetTest {
SearchResultProto searchResultProto = SearchResultProto.newBuilder()
.addResults(resultProto)
.build();
SearchResults searchResults = new SearchResults(searchResultProto);
// Making ResultReader and getting Snippet values.
while (searchResults.hasNext()) {
SearchResults.Result result = searchResults.next();
MatchInfo match = result.getMatchInfo().get(0);
for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
SearchResult.MatchInfo match = result.getMatches().get(0);
assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
assertThat(match.getFullText()).isEqualTo(propertyValueString);
assertThat(match.getExactMatch()).isEqualTo(exactMatch);
assertThat(match.getExactMatchPosition()).isEqualTo(
new SearchResult.MatchRange(/*lower=*/29, /*upper=*/32));
assertThat(match.getFullText()).isEqualTo(propertyValueString);
assertThat(match.getSnippetPosition()).isEqualTo(
new SearchResult.MatchRange(/*lower=*/26, /*upper=*/32));
assertThat(match.getSnippet()).isEqualTo(window);
}
}
// TODO(sidchhabra): Add tests for Double and Long Snippets.
// TODO(tytytyww): Add tests for Double and Long Snippets.
@Test
public void testNoSnippets() {
public void testNoSnippets() throws Exception {
final String propertyKeyString = "content";
final String propertyValueString = "A commonly used fake word is foo.\n"
@@ -117,16 +120,15 @@ public class SnippetTest {
SearchResultProto searchResultProto = SearchResultProto.newBuilder()
.addResults(resultProto)
.build();
SearchResults searchResults = new SearchResults(searchResultProto);
while (searchResults.hasNext()) {
SearchResults.Result result = searchResults.next();
assertThat(result.getMatchInfo()).isEqualTo(null);
for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
assertThat(result.getMatches()).isEqualTo(null);
}
}
@Test
public void testMultipleStringSnippet() {
public void testMultipleStringSnippet() throws Exception {
final String searchWord = "Test";
// Building the SearchResult received from query.
@@ -178,22 +180,29 @@ public class SnippetTest {
SearchResultProto searchResultProto = SearchResultProto.newBuilder()
.addResults(resultProto)
.build();
SearchResults searchResults = new SearchResults(searchResultProto);
// Making ResultReader and getting Snippet values.
while (searchResults.hasNext()) {
SearchResults.Result result = searchResults.next();
for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
MatchInfo match1 = result.getMatchInfo().get(0);
SearchResult.MatchInfo match1 = result.getMatches().get(0);
assertThat(match1.getPropertyPath()).isEqualTo("sender.name");
assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
assertThat(match1.getExactMatchPosition()).isEqualTo(
new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4));
assertThat(match1.getExactMatch()).isEqualTo("Test");
assertThat(match1.getSnippetPosition()).isEqualTo(
new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9));
assertThat(match1.getSnippet()).isEqualTo("Test Name");
MatchInfo match2 = result.getMatchInfo().get(1);
SearchResult.MatchInfo match2 = result.getMatches().get(1);
assertThat(match2.getPropertyPath()).isEqualTo("sender.email");
assertThat(match2.getFullText()).isEqualTo("TestNameJr@gmail.com");
assertThat(match2.getExactMatchPosition()).isEqualTo(
new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
assertThat(match2.getExactMatch()).isEqualTo("TestNameJr@gmail.com");
assertThat(match2.getSnippetPosition()).isEqualTo(
new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
assertThat(match2.getSnippet()).isEqualTo("TestNameJr@gmail.com");
}
}