Merge "Merge GenericDocument and SearchResult work from Jetpack."
This commit is contained in:
committed by
Android (Google) Code Review
commit
286e15457f
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.android.internal.infra.AndroidFuture;
|
||||
|
||||
parcelable AppSearchResult;
|
||||
parcelable AppSearchBatchResult;
|
||||
parcelable SearchResults;
|
||||
|
||||
/** {@hide} */
|
||||
interface IAppSearchManager {
|
||||
@@ -41,7 +42,7 @@ interface IAppSearchManager {
|
||||
/**
|
||||
* Inserts documents into the index.
|
||||
*
|
||||
* @param documentsBytes {@link List}<byte[]> of serialized DocumentProtos.
|
||||
* @param documentBundes List of GenericDocument bundles.
|
||||
* @param callback
|
||||
* {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link Void}>>.
|
||||
* If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
|
||||
@@ -49,18 +50,19 @@ interface IAppSearchManager {
|
||||
* {@link AppSearchBatchResult}<{@link String}, {@link Void}>
|
||||
* where the keys are document URIs, and the values are {@code null}.
|
||||
*/
|
||||
void putDocuments(in List documentsBytes, in AndroidFuture<AppSearchBatchResult> callback);
|
||||
void putDocuments(
|
||||
in List<Bundle> documentBundles, in AndroidFuture<AppSearchBatchResult> callback);
|
||||
|
||||
/**
|
||||
* Retrieves documents from the index.
|
||||
*
|
||||
* @param uris The URIs of the documents to retrieve
|
||||
* @param callback
|
||||
* {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link byte[]}>>.
|
||||
* {@link AndroidFuture}<{@link AppSearchBatchResult}<{@link String}, {@link Bundle}>>.
|
||||
* If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
|
||||
* {@code callback} will be completed with an
|
||||
* {@link AppSearchBatchResult}<{@link String}, {@link byte[]}>
|
||||
* where the keys are document URIs, and the values are serialized Document protos.
|
||||
* {@link AppSearchBatchResult}<{@link String}, {@link Bundle}>
|
||||
* where the keys are document URIs, and the values are Document bundles.
|
||||
*/
|
||||
void getDocuments(in List<String> uris, in AndroidFuture<AppSearchBatchResult> callback);
|
||||
|
||||
@@ -69,8 +71,7 @@ interface IAppSearchManager {
|
||||
*
|
||||
* @param queryExpression String to search for
|
||||
* @param searchSpecBundle SearchSpec bundle
|
||||
* @param callback {@link AndroidFuture}<{@link AppSearchResult}<{@link byte[]}>>
|
||||
* Will be completed with a serialized {@link SearchResultsProto}.
|
||||
* @param callback {@link AndroidFuture}<{@link AppSearchResult}<{@link SearchResults}>>
|
||||
*/
|
||||
void query(
|
||||
in String queryExpression,
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.app.appsearch;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.util.Range;
|
||||
|
||||
import com.google.android.icing.proto.SnippetMatchProto;
|
||||
|
||||
/**
|
||||
* Snippet: It refers to a substring of text from the content of document that is returned as a
|
||||
* part of search result.
|
||||
* This class represents a match objects for any Snippets that might be present in
|
||||
* {@link SearchResults} from query. Using this class user can get the full text, exact matches and
|
||||
* Snippets of document content for a given match.
|
||||
*
|
||||
* <p>Class Example 1:
|
||||
* A document contains following text in property subject:
|
||||
* <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
|
||||
*
|
||||
* <p>If the queryExpression is "foo".
|
||||
*
|
||||
* <p>{@link MatchInfo#getPropertyPath()} returns "subject"
|
||||
* <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another nonsense
|
||||
* word that’s used a lot is bar."
|
||||
* <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32]
|
||||
* <p>{@link MatchInfo#getExactMatch()} returns "foo"
|
||||
* <p>{@link MatchInfo#getSnippetPosition()} returns [29, 41]
|
||||
* <p>{@link MatchInfo#getSnippet()} returns "is foo. Another"
|
||||
* <p>
|
||||
* <p>Class Example 2:
|
||||
* A document contains a property name sender which contains 2 property names name and email, so
|
||||
* we will have 2 property paths: {@code sender.name} and {@code sender.email}.
|
||||
* <p> Let {@code sender.name = "Test Name Jr."} and {@code sender.email = "TestNameJr@gmail.com"}
|
||||
*
|
||||
* <p>If the queryExpression is "Test". We will have 2 matches.
|
||||
*
|
||||
* <p> Match-1
|
||||
* <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
|
||||
* <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
|
||||
* <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4]
|
||||
* <p>{@link MatchInfo#getExactMatch()} returns "Test"
|
||||
* <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
|
||||
* <p>{@link MatchInfo#getSnippet()} returns "Test Name Jr."
|
||||
* <p> Match-2
|
||||
* <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
|
||||
* <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
|
||||
* <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20]
|
||||
* <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
|
||||
* <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
|
||||
* <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
|
||||
* @hide
|
||||
*/
|
||||
// TODO(sidchhabra): Capture real snippet after integration with icingLib.
|
||||
public final class MatchInfo {
|
||||
|
||||
private final String mPropertyPath;
|
||||
private final SnippetMatchProto mSnippetMatch;
|
||||
private final AppSearchDocument mDocument;
|
||||
/**
|
||||
* List of content with same property path in a document when there are multiple matches in
|
||||
* repeated sections.
|
||||
*/
|
||||
private final String[] mValues;
|
||||
|
||||
/** @hide */
|
||||
public MatchInfo(@NonNull String propertyPath, @NonNull SnippetMatchProto snippetMatch,
|
||||
@NonNull AppSearchDocument document) {
|
||||
mPropertyPath = propertyPath;
|
||||
mSnippetMatch = snippetMatch;
|
||||
mDocument = document;
|
||||
// In IcingLib snippeting is available for only 3 data types i.e String, double and long,
|
||||
// so we need to check which of these three are requested.
|
||||
// TODO (sidchhabra): getPropertyStringArray takes property name, handle for property path.
|
||||
String[] values = mDocument.getPropertyStringArray(propertyPath);
|
||||
if (values == null) {
|
||||
values = doubleToString(mDocument.getPropertyDoubleArray(propertyPath));
|
||||
}
|
||||
if (values == null) {
|
||||
values = longToString(mDocument.getPropertyLongArray(propertyPath));
|
||||
}
|
||||
if (values == null) {
|
||||
throw new IllegalStateException("No content found for requested property path!");
|
||||
}
|
||||
mValues = values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the property path corresponding to the given entry.
|
||||
* <p>Property Path: '.' - delimited sequence of property names indicating which property in
|
||||
* the Document these snippets correspond to.
|
||||
* <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
|
||||
* For class example 1 this returns "subject"
|
||||
*/
|
||||
@NonNull
|
||||
public String getPropertyPath() {
|
||||
return mPropertyPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the full text corresponding to the given entry.
|
||||
* <p>For class example this returns "A commonly used fake word is foo. Another nonsense word
|
||||
* that’s used a lot is bar."
|
||||
*/
|
||||
@NonNull
|
||||
public String getFullText() {
|
||||
return mValues[mSnippetMatch.getValuesIndex()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the exact match range corresponding to the given entry.
|
||||
* <p>For class example 1 this returns [29, 32]
|
||||
*/
|
||||
@NonNull
|
||||
public Range getExactMatchPosition() {
|
||||
return new Range(mSnippetMatch.getExactMatchPosition(),
|
||||
mSnippetMatch.getExactMatchPosition() + mSnippetMatch.getExactMatchBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the exact match corresponding to the given entry.
|
||||
* <p>For class example 1 this returns "foo"
|
||||
*/
|
||||
@NonNull
|
||||
public CharSequence getExactMatch() {
|
||||
return getSubstring(getExactMatchPosition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snippet range corresponding to the given entry.
|
||||
* <p>For class example 1 this returns [29, 41]
|
||||
*/
|
||||
@NonNull
|
||||
public Range getSnippetPosition() {
|
||||
return new Range(mSnippetMatch.getWindowPosition(),
|
||||
mSnippetMatch.getWindowPosition() + mSnippetMatch.getWindowBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snippet corresponding to the given entry.
|
||||
* <p>Snippet - Provides a subset of the content to display. The
|
||||
* length of this content can be changed {@link SearchSpec.Builder#setMaxSnippetSize(int)}.
|
||||
* Windowing is centered around the middle of the matched token with content on either side
|
||||
* clipped to token boundaries.
|
||||
* <p>For class example 1 this returns "foo. Another"
|
||||
*/
|
||||
@NonNull
|
||||
public CharSequence getSnippet() {
|
||||
return getSubstring(getSnippetPosition());
|
||||
}
|
||||
|
||||
private CharSequence getSubstring(Range range) {
|
||||
return getFullText()
|
||||
.substring((int) range.getLower(), (int) range.getUpper());
|
||||
}
|
||||
|
||||
/** Utility method to convert double[] to String[] */
|
||||
private String[] doubleToString(double[] values) {
|
||||
//TODO(sidchhabra): Implement the method.
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Utility method to convert long[] to String[] */
|
||||
private String[] longToString(long[] values) {
|
||||
//TODO(sidchhabra): Implement the method.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.app.appsearch;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class represents one of the results obtained from the query.
|
||||
*
|
||||
* <p>It contains the document which matched, information about which section(s) in the document
|
||||
* matched, and snippet information containing textual summaries of the document's match(es).
|
||||
* @hide
|
||||
*/
|
||||
public final class SearchResult {
|
||||
/** @hide */
|
||||
|
||||
public static final String DOCUMENT_FIELD = "document";
|
||||
|
||||
/** @hide */
|
||||
|
||||
public static final String MATCHES_FIELD = "matches";
|
||||
|
||||
@NonNull
|
||||
private final Bundle mBundle;
|
||||
|
||||
@NonNull
|
||||
private final Bundle mDocumentBundle;
|
||||
|
||||
@Nullable
|
||||
private GenericDocument mDocument;
|
||||
|
||||
@Nullable
|
||||
private final List<Bundle> mMatchBundles;
|
||||
|
||||
/**
|
||||
* Contains a list of Snippets that matched the request. Only populated when requested in
|
||||
* both {@link SearchSpec.Builder#setSnippetCount(int)}
|
||||
* and {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}.
|
||||
*
|
||||
* @see #getMatches()
|
||||
*/
|
||||
@Nullable
|
||||
private List<MatchInfo> mMatches;
|
||||
|
||||
/** @hide */
|
||||
|
||||
public SearchResult(@NonNull Bundle bundle) {
|
||||
mBundle = Preconditions.checkNotNull(bundle);
|
||||
mDocumentBundle = Preconditions.checkNotNull(bundle.getBundle(DOCUMENT_FIELD));
|
||||
mMatchBundles = bundle.getParcelableArrayList(MATCHES_FIELD);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
|
||||
@NonNull
|
||||
public Bundle getBundle() {
|
||||
return mBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the matching {@link GenericDocument}.
|
||||
* @return Document object which matched the query.
|
||||
*/
|
||||
@NonNull
|
||||
public GenericDocument getDocument() {
|
||||
if (mDocument == null) {
|
||||
mDocument = new GenericDocument(mDocumentBundle);
|
||||
}
|
||||
return mDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains a list of Snippets that matched the request. Only populated when requested in
|
||||
* both {@link SearchSpec.Builder#setSnippetCount(int)}
|
||||
* and {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}.
|
||||
*
|
||||
* @return List of matches based on {@link SearchSpec}, if snippeting is disabled and this
|
||||
* method is called it will return {@code null}. Users can also restrict snippet population
|
||||
* using {@link SearchSpec.Builder#setSnippetCount} and
|
||||
* {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}, for all results after that
|
||||
* value this method will return {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public List<MatchInfo> getMatches() {
|
||||
if (mMatchBundles != null && mMatches == null) {
|
||||
mMatches = new ArrayList<>(mMatchBundles.size());
|
||||
for (int i = 0; i < mMatchBundles.size(); i++) {
|
||||
MatchInfo matchInfo = new MatchInfo(getDocument(), mMatchBundles.get(i));
|
||||
mMatches.add(matchInfo);
|
||||
}
|
||||
}
|
||||
return mMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snippet: It refers to a substring of text from the content of document that is returned as a
|
||||
* part of search result.
|
||||
* This class represents a match objects for any Snippets that might be present in
|
||||
* {@link SearchResults} from query. Using this class user can get the full text, exact matches
|
||||
* and Snippets of document content for a given match.
|
||||
*
|
||||
* <p>Class Example 1:
|
||||
* A document contains following text in property subject:
|
||||
* <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
|
||||
*
|
||||
* <p>If the queryExpression is "foo".
|
||||
*
|
||||
* <p>{@link MatchInfo#getPropertyPath()} returns "subject"
|
||||
* <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
|
||||
* nonsense word that’s used a lot is bar."
|
||||
* <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32]
|
||||
* <p>{@link MatchInfo#getExactMatch()} returns "foo"
|
||||
* <p>{@link MatchInfo#getSnippetPosition()} returns [26, 33]
|
||||
* <p>{@link MatchInfo#getSnippet()} returns "is foo."
|
||||
* <p>
|
||||
* <p>Class Example 2:
|
||||
* A document contains a property name sender which contains 2 property names name and email, so
|
||||
* we will have 2 property paths: {@code sender.name} and {@code sender.email}.
|
||||
* <p>Let {@code sender.name = "Test Name Jr."} and
|
||||
* {@code sender.email = "TestNameJr@gmail.com"}
|
||||
*
|
||||
* <p>If the queryExpression is "Test". We will have 2 matches.
|
||||
*
|
||||
* <p> Match-1
|
||||
* <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
|
||||
* <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
|
||||
* <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4]
|
||||
* <p>{@link MatchInfo#getExactMatch()} returns "Test"
|
||||
* <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
|
||||
* <p>{@link MatchInfo#getSnippet()} returns "Test Name"
|
||||
* <p> Match-2
|
||||
* <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
|
||||
* <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
|
||||
* <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20]
|
||||
* <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
|
||||
* <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
|
||||
* <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
|
||||
*/
|
||||
public static final class MatchInfo {
|
||||
/**
|
||||
* The path of the matching snippet property.
|
||||
* @hide
|
||||
*/
|
||||
|
||||
public static final String PROPERTY_PATH_FIELD = "propertyPath";
|
||||
|
||||
/**
|
||||
* The index of matching value in its property. A property may have multiple values. This
|
||||
* index indicates which value is the match.
|
||||
* @hide
|
||||
*/
|
||||
|
||||
public static final String VALUES_INDEX_FIELD = "valuesIndex";
|
||||
|
||||
/** @hide */
|
||||
|
||||
public static final String EXACT_MATCH_POSITION_LOWER_FIELD = "exactMatchPositionLower";
|
||||
|
||||
/** @hide */
|
||||
|
||||
public static final String EXACT_MATCH_POSITION_UPPER_FIELD = "exactMatchPositionUpper";
|
||||
|
||||
/** @hide */
|
||||
|
||||
public static final String WINDOW_POSITION_LOWER_FIELD = "windowPositionLower";
|
||||
|
||||
/** @hide */
|
||||
|
||||
public static final String WINDOW_POSITION_UPPER_FIELD = "windowPositionUpper";
|
||||
|
||||
private final String mFullText;
|
||||
private final String mPropertyPath;
|
||||
private final Bundle mBundle;
|
||||
private MatchRange mExactMatchRange;
|
||||
private MatchRange mWindowRange;
|
||||
|
||||
MatchInfo(@NonNull GenericDocument document, @NonNull Bundle bundle) {
|
||||
mBundle = Preconditions.checkNotNull(bundle);
|
||||
Preconditions.checkNotNull(document);
|
||||
mPropertyPath = Preconditions.checkNotNull(bundle.getString(PROPERTY_PATH_FIELD));
|
||||
mFullText = getPropertyValues(
|
||||
document, mPropertyPath, mBundle.getInt(VALUES_INDEX_FIELD));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the property path corresponding to the given entry.
|
||||
* <p>Property Path: '.' - delimited sequence of property names indicating which property in
|
||||
* the Document these snippets correspond to.
|
||||
* <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
|
||||
* For class example 1 this returns "subject"
|
||||
*/
|
||||
@NonNull
|
||||
public String getPropertyPath() {
|
||||
return mPropertyPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the full text corresponding to the given entry.
|
||||
* <p>For class example this returns "A commonly used fake word is foo. Another nonsense
|
||||
* word that's used a lot is bar."
|
||||
*/
|
||||
@NonNull
|
||||
public String getFullText() {
|
||||
return mFullText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the exact {@link MatchRange} corresponding to the given entry.
|
||||
* <p>For class example 1 this returns [29, 32]
|
||||
*/
|
||||
@NonNull
|
||||
public MatchRange getExactMatchPosition() {
|
||||
if (mExactMatchRange == null) {
|
||||
mExactMatchRange = new MatchRange(
|
||||
mBundle.getInt(EXACT_MATCH_POSITION_LOWER_FIELD),
|
||||
mBundle.getInt(EXACT_MATCH_POSITION_UPPER_FIELD));
|
||||
}
|
||||
return mExactMatchRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link MatchRange} corresponding to the given entry.
|
||||
* <p>For class example 1 this returns "foo"
|
||||
*/
|
||||
@NonNull
|
||||
public CharSequence getExactMatch() {
|
||||
return getSubstring(getExactMatchPosition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snippet {@link MatchRange} corresponding to the given entry.
|
||||
* <p>Only populated when set maxSnippetSize > 0 in
|
||||
* {@link SearchSpec.Builder#setMaxSnippetSize}.
|
||||
* <p>For class example 1 this returns [29, 41].
|
||||
*/
|
||||
@NonNull
|
||||
public MatchRange getSnippetPosition() {
|
||||
if (mWindowRange == null) {
|
||||
mWindowRange = new MatchRange(
|
||||
mBundle.getInt(WINDOW_POSITION_LOWER_FIELD),
|
||||
mBundle.getInt(WINDOW_POSITION_UPPER_FIELD));
|
||||
}
|
||||
return mWindowRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the snippet corresponding to the given entry.
|
||||
* <p>Snippet - Provides a subset of the content to display. Only populated when requested
|
||||
* maxSnippetSize > 0. The size of this content can be changed by
|
||||
* {@link SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of
|
||||
* the matched token with content on either side clipped to token boundaries.
|
||||
* <p>For class example 1 this returns "foo. Another"
|
||||
*/
|
||||
@NonNull
|
||||
public CharSequence getSnippet() {
|
||||
return getSubstring(getSnippetPosition());
|
||||
}
|
||||
|
||||
private CharSequence getSubstring(MatchRange range) {
|
||||
return getFullText().substring(range.getStart(), range.getEnd());
|
||||
}
|
||||
|
||||
/** Extracts the matching string from the document. */
|
||||
private static String getPropertyValues(
|
||||
GenericDocument document, String propertyName, int valueIndex) {
|
||||
// In IcingLib snippeting is available for only 3 data types i.e String, double and
|
||||
// long, so we need to check which of these three are requested.
|
||||
// TODO (tytytyww): getPropertyStringArray takes property name, handle for property
|
||||
// path.
|
||||
// TODO (tytytyww): support double[] and long[].
|
||||
String[] values = document.getPropertyStringArray(propertyName);
|
||||
if (values == null) {
|
||||
throw new IllegalStateException("No content found for requested property path!");
|
||||
}
|
||||
return values[valueIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class providing the position range of matching information.
|
||||
*
|
||||
* <p> All ranges are finite, and the left side of the range is always {@code <=} the right
|
||||
* side of the range.
|
||||
*
|
||||
* <p> Example: MatchRange(0, 100) represent a hundred ints from 0 to 99."
|
||||
*
|
||||
*/
|
||||
public static final class MatchRange {
|
||||
private final int mEnd;
|
||||
private final int mStart;
|
||||
|
||||
/**
|
||||
* Creates a new immutable range.
|
||||
* <p> The endpoints are {@code [start, end)}; that is the range is bounded. {@code start}
|
||||
* must be lesser or equal to {@code end}.
|
||||
*
|
||||
* @param start The start point (inclusive)
|
||||
* @param end The end point (exclusive)
|
||||
* @hide
|
||||
*/
|
||||
|
||||
public MatchRange(int start, int end) {
|
||||
if (start > end) {
|
||||
throw new IllegalArgumentException("Start point must be less than or equal to "
|
||||
+ "end point");
|
||||
}
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
}
|
||||
|
||||
/** Gets the start point (inclusive). */
|
||||
public int getStart() {
|
||||
return mStart;
|
||||
}
|
||||
|
||||
/** Gets the end point (exclusive). */
|
||||
public int getEnd() {
|
||||
return mEnd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof MatchRange)) {
|
||||
return false;
|
||||
}
|
||||
MatchRange otherMatchRange = (MatchRange) other;
|
||||
return this.getStart() == otherMatchRange.getStart()
|
||||
&& this.getEnd() == otherMatchRange.getEnd();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return "MatchRange { start: " + mStart + " , end: " + mEnd + "}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mStart, mEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
30
core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java
vendored
Normal file
30
core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user