Add AppSearch.java, containing the Document class, base Builder, and Email.

Document is the basic entity to be indexed and queried in AppSearch.
AppSearch.java contains all AppSearch Document types and their Builders.
They are grouped here to avoid potential collisions with other
generically-named classes like Email and Contact).

Bug: 143789408
Test: atest FrameworksCoreTests:android.app.appsearch
Change-Id: I7bb607ad4114451a3db11b9ade9c197da1e6556a
This commit is contained in:
Alexander Dorokhine
2020-01-11 00:17:48 -08:00
committed by Terry Wang
parent 9ebb72c7cc
commit 0731f57193
8 changed files with 1163 additions and 0 deletions

View File

@@ -0,0 +1,762 @@
/*
* 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.CurrentTimeSecondsLong;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.PropertyProto;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Collection of all AppSearch Document Types.
*
* @hide
*/
// TODO(b/143789408) Spilt this class to make all subclasses to their own file.
public final class AppSearch {
private AppSearch() {}
/**
* Represents a document unit.
*
* <p>Documents are constructed via {@link Document.Builder}.
*
* @hide
*/
// TODO(b/143789408) set TTL for document in mProtoBuilder
// TODO(b/144518768) add visibility field if the stakeholders are comfortable with a no-op
// opt-in for this release.
public static class Document {
private static final String TAG = "AppSearch.Document";
/**
* 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 Document} basic information (uri, schemaType etc) and properties ordered
* by keys.
*/
@NonNull
private final DocumentProto mProto;
/** Contains all properties in {@link #mProto} to support get properties via keys. */
@NonNull
private final Bundle mPropertyBundle;
/**
* Create a new {@link Document}.
* @param proto Contains {@link Document} basic information (uri, schemaType etc) and
* properties ordered by keys.
* @param propertyBundle Contains all properties in {@link #mProto} to support get
* properties via keys.
*/
private Document(@NonNull DocumentProto proto, @NonNull Bundle propertyBundle) {
this.mProto = proto;
this.mPropertyBundle = propertyBundle;
}
/**
* Create a new {@link Document} from an existing instance.
*
* <p>This method should be only used by constructor of a subclass.
*/
// TODO(b/143789408) add constructor take DocumentProto to create a document.
protected Document(@NonNull Document document) {
this(document.mProto, document.mPropertyBundle);
}
/**
* Creates a new {@link Document.Builder}.
*
* @param uri The uri of {@link Document}.
* @param schemaType The schema type of the {@link Document}. 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#put}. Otherwise, the document will be rejected by
* {@link AppSearchManager#put}.
* @hide
*/
@NonNull
public static Builder newBuilder(@NonNull String uri, @NonNull String schemaType) {
return new Builder(uri, schemaType);
}
/**
* Get the {@link DocumentProto} of the {@link Document}.
*
* <p>The {@link DocumentProto} contains {@link Document}'s basic information and all
* properties ordered by keys.
* @hide
*/
@NonNull
@VisibleForTesting
public DocumentProto getProto() {
return mProto;
}
/**
* Get the uri of the {@link Document}.
*
* @hide
*/
@NonNull
public String getUri() {
return mProto.getUri();
}
/**
* Get the schema type of the {@link Document}.
* @hide
*/
@NonNull
public String getSchemaType() {
return mProto.getSchema();
}
/**
* Get the creation timestamp in seconds of the {@link Document}.
*
* @hide
*/
// TODO(b/143789408) Change seconds to millis with Icing library.
@CurrentTimeSecondsLong
public long getCreationTimestampSecs() {
return mProto.getCreationTimestampSecs();
}
/**
* Returns the score of the {@link Document}.
*
* <p>The score is a query-independent measure of the document's quality, relative to other
* {@link Document}s of the same type.
*
* <p>The default value is 0.
*
* @hide
*/
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.
* @hide
*/
@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 {@link Long} value by key.
*
* @param key The key to look for.
* @return The first {@link Long} associated with the given key or {@code null} if there
* is no such key or the value is of a different type.
* @hide
*/
@Nullable
public Long getPropertyLong(@NonNull String key) {
long[] propertyArray = getPropertyLongArray(key);
if (ArrayUtils.isEmpty(propertyArray)) {
return null;
}
warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
return propertyArray[0];
}
/**
* Retrieve a {@link Double} value by key.
*
* @param key The key to look for.
* @return The first {@link Double} associated with the given key or {@code null} if there
* is no such key or the value is of a different type.
* @hide
*/
@Nullable
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 null;
}
warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
return propertyArray[0];
}
/**
* Retrieve a {@link Boolean} value by key.
*
* @param key The key to look for.
* @return The first {@link Boolean} associated with the given key or {@code null} if there
* is no such key or the value is of a different type.
* @hide
*/
@Nullable
public Boolean getPropertyBoolean(@NonNull String key) {
boolean[] propertyArray = getPropertyBooleanArray(key);
if (ArrayUtils.isEmpty(propertyArray)) {
return null;
}
warnIfSinglePropertyTooLong("Boolean", 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 {@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.
* @hide
*/
@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.
* @hide
*/
@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.
* @hide
*/
@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.
* @hide
*/
@Nullable
public boolean[] getPropertyBooleanArray(@NonNull String key) {
return getAndCastPropertyArray(key, boolean[].class);
}
/**
* Gets a repeated property of the given key, and casts it to the given class type, which
* must be an array class type.
*/
@Nullable
private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
Object value = mPropertyBundle.get(key);
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
// mPropertyBundle are ordered by keys and stored in proto.
if (this == other) {
return true;
}
if (!(other instanceof Document)) {
return false;
}
Document otherDocument = (Document) other;
return this.mProto.equals(otherDocument.mProto);
}
@Override
public int hashCode() {
// Hash only proto is sufficient here since all properties in mPropertyBundle are
// ordered by keys and stored in proto.
return mProto.hashCode();
}
@Override
public String toString() {
return mProto.toString();
}
/**
* The builder class for {@link Document}.
*
* @param <BuilderType> Type of subclass who extend this.
* @hide
*/
public static class Builder<BuilderType extends Builder> {
private final Bundle mPropertyBundle = new Bundle();
private final DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
private final BuilderType mBuilderTypeInstance;
/**
* Create a new {@link Document.Builder}.
*
* @param uri The uri of {@link Document}.
* @param schemaType The schema type of the {@link Document}. 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#put}. Otherwise, the document will be rejected by
* {@link AppSearchManager#put}.
* @hide
*/
protected Builder(@NonNull String uri, @NonNull String schemaType) {
mBuilderTypeInstance = (BuilderType) this;
mProtoBuilder.setUri(uri).setSchema(schemaType);
// Set current timestamp for creation timestamp by default.
setCreationTimestampSecs(
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
}
/**
* Set the score of the {@link Document}.
*
* <p>The score is a query-independent measure of the document's quality, relative to
* other {@link Document}s of the same type.
*
* @throws IllegalArgumentException If the provided value is negative.
* @hide
*/
@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 seconds of the {@link Document}.
*
* @hide
*/
@NonNull
public BuilderType setCreationTimestampSecs(
@CurrentTimeSecondsLong long creationTimestampSecs) {
mProtoBuilder.setCreationTimestampSecs(creationTimestampSecs);
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.
* @hide
*/
@NonNull
public BuilderType setProperty(@NonNull String key, @NonNull String... values) {
putInBundle(mPropertyBundle, 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 schema.org property.
* @hide
*/
@NonNull
public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) {
putInBundle(mPropertyBundle, 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 schema.org property.
* @hide
*/
@NonNull
public BuilderType setProperty(@NonNull String key, @NonNull long... values) {
putInBundle(mPropertyBundle, 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 schema.org property.
* @hide
*/
@NonNull
public BuilderType setProperty(@NonNull String key, @NonNull double... values) {
putInBundle(mPropertyBundle, key, values);
return mBuilderTypeInstance;
}
private static void putInBundle(
@NonNull Bundle bundle, @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 + ".");
}
}
bundle.putStringArray(key, values);
}
private static void putInBundle(
@NonNull Bundle bundle, @NonNull String key, @NonNull boolean... values) {
Objects.requireNonNull(key);
Objects.requireNonNull(values);
validateRepeatedPropertyLength(key, values.length);
bundle.putBooleanArray(key, values);
}
private static void putInBundle(
@NonNull Bundle bundle, @NonNull String key, @NonNull double... values) {
Objects.requireNonNull(key);
Objects.requireNonNull(values);
validateRepeatedPropertyLength(key, values.length);
bundle.putDoubleArray(key, values);
}
private static void putInBundle(
@NonNull Bundle bundle, @NonNull String key, @NonNull long... values) {
Objects.requireNonNull(key);
Objects.requireNonNull(values);
validateRepeatedPropertyLength(key, values.length);
bundle.putLongArray(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 Document} object.
* @hide
*/
public Document build() {
// Build proto by sorting the keys in propertyBundle to exclude the influence of
// order. Therefore documents will generate same proto as long as the contents are
// same. Note that the order of repeated fields is still preserved.
ArrayList<String> keys = new ArrayList<>(mPropertyBundle.keySet());
Collections.sort(keys);
for (String key : keys) {
Object values = mPropertyBundle.get(key);
PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(key);
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 {
throw new IllegalStateException(
"Property \"" + key + "\" has unsupported value type \""
+ values.getClass().getSimpleName() + "\"");
}
mProtoBuilder.addProperties(propertyProto);
}
return new Document(mProtoBuilder.build(), mPropertyBundle);
}
}
}
/**
* Encapsulates a {@link Document} that represent an email.
*
* <p>This class is a higher level implement of {@link Document}.
*
* <p>This class will eventually migrate to Jetpack, where it will become public API.
*
* @hide
*/
public static class Email extends Document {
/** The name of the schema type for {@link Email} 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";
private static final String KEY_BCC = "bcc";
private static final String KEY_SUBJECT = "subject";
private static final String KEY_BODY = "body";
/**
* Creates a new {@link Email} from the contents of an existing {@link Document}.
*
* @param document The {@link Document} containing the email content.
*/
public Email(@NonNull Document document) {
super(document);
}
/**
* Creates a new {@link Email.Builder}.
*
* @param uri The uri of {@link Email}.
*/
public static Builder newBuilder(@NonNull String uri) {
return new Builder(uri);
}
/**
* Get the from address of {@link Email}.
*
* @return Returns the subject of {@link Email} or {@code null} if it's not been set yet.
* @hide
*/
@Nullable
public String getFrom() {
return getPropertyString(KEY_FROM);
}
/**
* Get the destination address of {@link Email}.
*
* @return Returns the destination address of {@link Email} or {@code null} if it's not been
* set yet.
* @hide
*/
@Nullable
public String[] getTo() {
return getPropertyStringArray(KEY_TO);
}
/**
* Get the CC list of {@link Email}.
*
* @return Returns the CC list of {@link Email} or {@code null} if it's not been set yet.
* @hide
*/
@Nullable
public String[] getCc() {
return getPropertyStringArray(KEY_CC);
}
/**
* Get the BCC list of {@link Email}.
*
* @return Returns the BCC list of {@link Email} or {@code null} if it's not been set yet.
* @hide
*/
@Nullable
public String[] getBcc() {
return getPropertyStringArray(KEY_BCC);
}
/**
* Get the subject of {@link Email}.
*
* @return Returns the value subject of {@link Email} or {@code null} if it's not been set
* yet.
* @hide
*/
@Nullable
public String getSubject() {
return getPropertyString(KEY_SUBJECT);
}
/**
* Get the body of {@link Email}.
*
* @return Returns the body of {@link Email} or {@code null} if it's not been set yet.
* @hide
*/
@Nullable
public String getBody() {
return getPropertyString(KEY_BODY);
}
/**
* The builder class for {@link Email}.
* @hide
*/
public static class Builder extends Document.Builder<Email.Builder> {
/**
* Create a new {@link Email.Builder}
* @param uri The Uri of the Email.
* @hide
*/
private Builder(@NonNull String uri) {
super(uri, SCHEMA_TYPE);
}
/**
* Set the from address of {@link Email}
* @hide
*/
@NonNull
public Email.Builder setFrom(@NonNull String from) {
setProperty(KEY_FROM, from);
return this;
}
/**
* Set the destination address of {@link Email}
* @hide
*/
@NonNull
public Email.Builder setTo(@NonNull String... to) {
setProperty(KEY_TO, to);
return this;
}
/**
* Set the CC list of {@link Email}
* @hide
*/
@NonNull
public Email.Builder setCc(@NonNull String... cc) {
setProperty(KEY_CC, cc);
return this;
}
/**
* Set the BCC list of {@link Email}
* @hide
*/
@NonNull
public Email.Builder setBcc(@NonNull String... bcc) {
setProperty(KEY_BCC, bcc);
return this;
}
/**
* Set the subject of {@link Email}
* @hide
*/
@NonNull
public Email.Builder setSubject(@NonNull String subject) {
setProperty(KEY_SUBJECT, subject);
return this;
}
/**
* Set the body of {@link Email}
* @hide
*/
@NonNull
public Email.Builder setBody(@NonNull String body) {
setProperty(KEY_BODY, body);
return this;
}
/**
* Builds the {@link Email} object.
*
* @hide
*/
@NonNull
@Override
public Email build() {
return new Email(super.build());
}
}
}
}

View File

@@ -18,6 +18,7 @@ package android.app.appsearch;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.SystemService;
import android.app.appsearch.AppSearch.Document;
import android.content.Context;
import android.os.RemoteException;
@@ -25,6 +26,7 @@ import com.android.internal.infra.AndroidFuture;
import com.google.android.icing.proto.SchemaProto;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -95,4 +97,34 @@ public class AppSearchManager {
}
future.whenCompleteAsync((noop, err) -> callback.accept(err), executor);
}
/**
* Index {@link Document} to AppSearch
*
* <p>You should not call this method directly; instead, use the {@code AppSearch#put()} API
* provided by JetPack.
*
* <p>The schema should be set via {@link #setSchema} method.
*
* @param documents {@link Document Documents} that need to be indexed.
* @param executor Executor on which to invoke the callback.
* @param callback Callback to receive errors resulting from setting the schema. If the
* operation succeeds, the callback will be invoked with {@code null}.
*/
public void put(@NonNull List<Document> documents,
@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<? super Throwable> callback) {
AndroidFuture<Void> future = new AndroidFuture<>();
for (Document document : documents) {
// TODO(b/146386470) batching Document protos
try {
mService.put(document.getProto().toByteArray(), future);
} catch (RemoteException e) {
future.completeExceptionally(e);
break;
}
}
// TODO(b/147614371) Fix error report for multiple documents.
future.whenCompleteAsync((noop, err) -> callback.accept(err), executor);
}
}

View File

@@ -28,4 +28,5 @@ interface IAppSearchManager {
* if setSchema fails.
*/
void setSchema(in byte[] schemaProto, in AndroidFuture callback);
void put(in byte[] documentBytes, in AndroidFuture callback);
}

View File

@@ -43,6 +43,16 @@ public class AppSearchManagerService extends SystemService {
try {
SchemaProto schema = SchemaProto.parseFrom(schemaBytes);
throw new UnsupportedOperationException("setSchema not yet implemented: " + schema);
} catch (Throwable t) {
callback.completeExceptionally(t);
}
}
@Override
public void put(byte[] documentBytes, AndroidFuture callback) {
try {
throw new UnsupportedOperationException("Put document not yet implemented");
} catch (Throwable t) {
callback.completeExceptionally(t);
}

View File

@@ -10,6 +10,14 @@
"include-filter": "com.android.server.appsearch"
}
]
},
{
"name": "FrameworksCoreTests",
"options": [
{
"include-filter": "android.app.appsearch"
}
]
}
]
}

View File

@@ -0,0 +1,211 @@
/*
* 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.AppSearch.Document;
import androidx.test.filters.SmallTest;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.PropertyProto;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@SmallTest
public class AppSearchDocumentTest {
@Test
public void testDocumentEquals_Identical() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
.setCreationTimestampSecs(0L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.build();
Document document2 = Document.newBuilder("uri1", "schemaType1")
.setCreationTimestampSecs(0L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.build();
assertThat(document1).isEqualTo(document2);
assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
}
@Test
public void testDocumentEquals_DifferentOrder() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
.setCreationTimestampSecs(0L)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.build();
// Create second document with same parameter but different order.
Document document2 = Document.newBuilder("uri1", "schemaType1")
.setCreationTimestampSecs(0L)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("longKey1", 1L, 2L, 3L)
.build();
assertThat(document1).isEqualTo(document2);
assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
}
@Test
public void testDocumentEquals_Failure() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
.setProperty("longKey1", 1L, 2L, 3L)
.build();
// Create second document with same order but different value.
Document document2 = Document.newBuilder("uri1", "schemaType1")
.setProperty("longKey1", 1L, 2L, 4L) // Different
.build();
assertThat(document1).isNotEqualTo(document2);
assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
}
@Test
public void testDocumentEquals_Failure_RepeatedFieldOrder() {
Document document1 = Document.newBuilder("uri1", "schemaType1")
.setProperty("booleanKey1", true, false, true)
.build();
// Create second document with same order but different value.
Document document2 = Document.newBuilder("uri1", "schemaType1")
.setProperty("booleanKey1", true, true, false) // Different
.build();
assertThat(document1).isNotEqualTo(document2);
assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
}
@Test
public void testDocumentGetSingleValue() {
Document document = Document.newBuilder("uri1", "schemaType1")
.setProperty("longKey1", 1L)
.setProperty("doubleKey1", 1.0)
.setProperty("booleanKey1", true)
.setProperty("stringKey1", "test-value1").build();
assertThat(document.getUri()).isEqualTo("uri1");
assertThat(document.getSchemaType()).isEqualTo("schemaType1");
assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
}
@Test
public void testDocumentGetArrayValues() {
Document document = Document.newBuilder("uri1", "schemaType1")
.setScore(1)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.build();
assertThat(document.getUri()).isEqualTo("uri1");
assertThat(document.getSchemaType()).isEqualTo("schemaType1");
assertThat(document.getScore()).isEqualTo(1);
assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L, 2L, 3L);
assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
.containsExactly(1.0, 2.0, 3.0);
assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
.containsExactly(true, false, true);
assertThat(document.getPropertyStringArray("stringKey1")).asList()
.containsExactly("test-value1", "test-value2", "test-value3");
}
@Test
public void testDocumentGetValues_DifferentTypes() {
Document document = Document.newBuilder("uri1", "schemaType1")
.setScore(1)
.setProperty("longKey1", 1L)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.build();
// Get a value for a key that doesn't exist
assertThat(document.getPropertyDouble("doubleKey1")).isNull();
assertThat(document.getPropertyDoubleArray("doubleKey1")).isNull();
// Get a value with a single element as an array and as a single value
assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L);
// Get a value with multiple elements as an array and as a single value
assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
assertThat(document.getPropertyStringArray("stringKey1")).asList()
.containsExactly("test-value1", "test-value2", "test-value3");
// Get a value of the wrong type
assertThat(document.getPropertyDouble("longKey1")).isNull();
assertThat(document.getPropertyDoubleArray("longKey1")).isNull();
}
@Test
public void testDocumentInvalid() {
Document.Builder builder = Document.newBuilder("uri1", "schemaType1");
assertThrows(
IllegalArgumentException.class, () -> builder.setProperty("test", new boolean[]{}));
}
@Test
public void testDocumentProtoPopulation() {
Document document = Document.newBuilder("uri1", "schemaType1")
.setScore(1)
.setCreationTimestampSecs(0)
.setProperty("longKey1", 1L)
.setProperty("doubleKey1", 1.0)
.setProperty("booleanKey1", true)
.setProperty("stringKey1", "test-value1")
.build();
// Create the Document proto. Need to sort the property order by key.
DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
.setUri("uri1").setSchema("schemaType1").setScore(1).setCreationTimestampSecs(0);
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"));
List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
Collections.sort(sortedKey);
for (String key : sortedKey) {
documentProtoBuilder.addProperties(propertyProtoMap.get(key));
}
assertThat(document.getProto()).isEqualTo(documentProtoBuilder.build());
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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 android.app.appsearch.AppSearch.Email;
import androidx.test.filters.SmallTest;
import org.junit.Test;
@SmallTest
public class AppSearchEmailTest {
@Test
public void testBuildEmailAndGetValue() {
Email email = Email.newBuilder("uri")
.setFrom("FakeFromAddress")
.setCc("CC1", "CC2")
// Score and Property are mixed into the middle to make sure DocumentBuilder's
// methods can be interleaved with EmailBuilder's methods.
.setScore(1)
.setProperty("propertyKey", "propertyValue1", "propertyValue2")
.setSubject("subject")
.setBody("EmailBody")
.build();
assertThat(email.getUri()).isEqualTo("uri");
assertThat(email.getFrom()).isEqualTo("FakeFromAddress");
assertThat(email.getTo()).isNull();
assertThat(email.getCc()).asList().containsExactly("CC1", "CC2");
assertThat(email.getBcc()).isNull();
assertThat(email.getScore()).isEqualTo(1);
assertThat(email.getPropertyString("propertyKey")).isEqualTo("propertyValue1");
assertThat(email.getPropertyStringArray("propertyKey")).asList().containsExactly(
"propertyValue1", "propertyValue2");
assertThat(email.getSubject()).isEqualTo("subject");
assertThat(email.getBody()).isEqualTo("EmailBody");
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.impl;
import static com.google.common.truth.Truth.assertThat;
import android.annotation.NonNull;
import android.app.appsearch.AppSearch.Document;
import androidx.test.filters.SmallTest;
import org.junit.Test;
/** Tests that {@link Document} and {@link Document.Builder} are extendable by developers.
*
* <p>This class is intentionally in a different package than {@link Document} to make sure there
* are no package-private methods required for external developers to add custom types.
*/
@SmallTest
public class CustomerDocumentTest {
@Test
public void testBuildCustomerDocument() {
CustomerDocument customerDocument = CustomerDocument.newBuilder("uri1")
.setScore(1)
.setCreationTimestampSecs(0)
.setProperty("longKey1", 1L, 2L, 3L)
.setProperty("doubleKey1", 1.0, 2.0, 3.0)
.setProperty("booleanKey1", true, false, true)
.setProperty("stringKey1", "test-value1", "test-value2", "test-value3")
.build();
assertThat(customerDocument.getUri()).isEqualTo("uri1");
assertThat(customerDocument.getSchemaType()).isEqualTo("customerDocument");
assertThat(customerDocument.getScore()).isEqualTo(1);
assertThat(customerDocument.getCreationTimestampSecs()).isEqualTo(0L);
assertThat(customerDocument.getPropertyLongArray("longKey1")).asList()
.containsExactly(1L, 2L, 3L);
assertThat(customerDocument.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
.containsExactly(1.0, 2.0, 3.0);
assertThat(customerDocument.getPropertyBooleanArray("booleanKey1")).asList()
.containsExactly(true, false, true);
assertThat(customerDocument.getPropertyStringArray("stringKey1")).asList()
.containsExactly("test-value1", "test-value2", "test-value3");
}
/**
* An example document type for test purposes, defined outside of
* {@link android.app.appsearch.AppSearch} (the way an external developer would define it).
*/
private static class CustomerDocument extends Document {
private CustomerDocument(Document document) {
super(document);
}
public static CustomerDocument.Builder newBuilder(String uri) {
return new CustomerDocument.Builder(uri);
}
public static class Builder extends Document.Builder<CustomerDocument.Builder> {
private Builder(@NonNull String uri) {
super(uri, "customerDocument");
}
@Override
public CustomerDocument build() {
return new CustomerDocument(super.build());
}
}
}
}