Merge "Add AppSearch.java, containing the Document class, base Builder, and Email."
diff --git a/framework/java/android/app/appsearch/AppSearch.java b/framework/java/android/app/appsearch/AppSearch.java
new file mode 100644
index 0000000..e779b69
--- /dev/null
+++ b/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.
+     *
+     * <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());
+            }
+        }
+    }
+}
diff --git a/framework/java/android/app/appsearch/AppSearchManager.java b/framework/java/android/app/appsearch/AppSearchManager.java
index 58bb605..83195dc 100644
--- a/framework/java/android/app/appsearch/AppSearchManager.java
+++ b/framework/java/android/app/appsearch/AppSearchManager.java
@@ -18,6 +18,7 @@
 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.google.android.icing.proto.SchemaProto;
 
+import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -95,4 +97,34 @@
         }
         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);
+    }
 }
diff --git a/framework/java/android/app/appsearch/IAppSearchManager.aidl b/framework/java/android/app/appsearch/IAppSearchManager.aidl
index 8085aa8..fc83d8c 100644
--- a/framework/java/android/app/appsearch/IAppSearchManager.aidl
+++ b/framework/java/android/app/appsearch/IAppSearchManager.aidl
@@ -28,4 +28,5 @@
      *     if setSchema fails.
      */
     void setSchema(in byte[] schemaProto, in AndroidFuture callback);
+    void put(in byte[] documentBytes, in AndroidFuture callback);
 }
diff --git a/service/java/com/android/server/appsearch/AppSearchManagerService.java b/service/java/com/android/server/appsearch/AppSearchManagerService.java
index 96316b3..042f051 100644
--- a/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -43,6 +43,16 @@
             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/testing/coretests/src/android/app/appsearch/AppSearchDocumentTest.java b/testing/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
new file mode 100644
index 0000000..2091d55
--- /dev/null
+++ b/testing/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<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());
+    }
+}
diff --git a/testing/coretests/src/android/app/appsearch/AppSearchEmailTest.java b/testing/coretests/src/android/app/appsearch/AppSearchEmailTest.java
new file mode 100644
index 0000000..c50b1da
--- /dev/null
+++ b/testing/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/testing/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java b/testing/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java
new file mode 100644
index 0000000..4ee4aa6
--- /dev/null
+++ b/testing/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.
+ *
+ * <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());
+            }
+        }
+    }
+}