diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearch.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearch.java new file mode 100644 index 0000000000000..e779b69750c2b --- /dev/null +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearch.java @@ -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. + * + *

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. + * + *

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}. + * + *

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}. + * + *

The score is a query-independent measure of the document's quality, relative to other + * {@link Document}s of the same type. + * + *

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 getAndCastPropertyArray(@NonNull String key, @NonNull Class 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 Type of subclass who extend this. + * @hide + */ + public static class 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}. + * + *

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 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. + * + *

This class is a higher level implement of {@link Document}. + * + *

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 { + + /** + * 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()); + } + } + } +} diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 58bb6056db985..83195dc73db66 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -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 + * + *

You should not call this method directly; instead, use the {@code AppSearch#put()} API + * provided by JetPack. + * + *

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 documents, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer callback) { + AndroidFuture 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); + } } diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index 8085aa8b006c2..fc83d8ccbd4ae 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -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); } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java index 96316b311912f..042f051df4d03 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -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); } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING b/apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING index 08811f804fd16..ca5b8841ea498 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING +++ b/apex/appsearch/service/java/com/android/server/appsearch/TEST_MAPPING @@ -10,6 +10,14 @@ "include-filter": "com.android.server.appsearch" } ] + }, + { + "name": "FrameworksCoreTests", + "options": [ + { + "include-filter": "android.app.appsearch" + } + ] } ] } diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java new file mode 100644 index 0000000000000..2091d556394d2 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java @@ -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 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 sortedKey = new ArrayList<>(propertyProtoMap.keySet()); + Collections.sort(sortedKey); + for (String key : sortedKey) { + documentProtoBuilder.addProperties(propertyProtoMap.get(key)); + } + assertThat(document.getProto()).isEqualTo(documentProtoBuilder.build()); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java b/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java new file mode 100644 index 0000000000000..c50b1da71d027 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java @@ -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"); + } +} diff --git a/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java new file mode 100644 index 0000000000000..4ee4aa6d527f6 --- /dev/null +++ b/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java @@ -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. + * + *

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 { + private Builder(@NonNull String uri) { + super(uri, "customerDocument"); + } + + @Override + public CustomerDocument build() { + return new CustomerDocument(super.build()); + } + } + } +}