Merge GenericDocument and SearchResult work from Jetpack.

Bug: 162450968
Test: AppSearchManagerTest
Change-Id: I5617ddad49fa27c6e45fc9742aa27607f0cb2413
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java
deleted file mode 100644
index 7d2b64e..0000000
--- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java
+++ /dev/null
@@ -1,698 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.appsearch;
-
-import android.annotation.CurrentTimeMillisLong;
-import android.annotation.DurationMillisLong;
-import android.annotation.IntRange;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.Preconditions;
-
-import com.google.android.icing.proto.DocumentProto;
-import com.google.android.icing.proto.PropertyProto;
-import com.google.protobuf.ByteString;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * Represents a document unit.
- *
- * <p>Documents are constructed via {@link AppSearchDocument.Builder}.
- * @hide
- */
-public class AppSearchDocument {
-    private static final String TAG = "AppSearchDocument";
-
-    /** The default empty namespace.*/
-    // TODO(adorokhine): Allow namespace to be specified in the document.
-    public static final String DEFAULT_NAMESPACE = "";
-
-    /**
-     * The maximum number of elements in a repeatable field. Will reject the request if exceed
-     * this limit.
-     */
-    private static final int MAX_REPEATED_PROPERTY_LENGTH = 100;
-
-    /**
-     * The maximum {@link String#length} of a {@link String} field. Will reject the request if
-     * {@link String}s longer than this.
-     */
-    private static final int MAX_STRING_LENGTH = 20_000;
-
-    /**
-     * Contains {@link AppSearchDocument} basic information (uri, schemaType etc) and properties
-     * ordered by keys.
-     */
-    @NonNull
-    private final DocumentProto mProto;
-
-    /** Contains all properties in {@link #mProto} to support getting properties via keys. */
-    @NonNull
-    private final Map<String, Object> mProperties;
-
-    /**
-     * Creates a new {@link AppSearchDocument}.
-     * @param proto Contains {@link AppSearchDocument} basic information (uri, schemaType etc) and
-     *               properties ordered by keys.
-     * @param propertiesMap Contains all properties in {@link #mProto} to support get properties
-     *                      via keys.
-     */
-    private AppSearchDocument(@NonNull DocumentProto proto,
-            @NonNull Map<String, Object> propertiesMap) {
-        mProto = proto;
-        mProperties = propertiesMap;
-    }
-
-    /**
-     * Creates a new {@link AppSearchDocument} from an existing instance.
-     *
-     * <p>This method should be only used by constructor of a subclass.
-     */
-    protected AppSearchDocument(@NonNull AppSearchDocument document) {
-        this(document.mProto, document.mProperties);
-    }
-
-    /** @hide */
-    AppSearchDocument(@NonNull DocumentProto documentProto) {
-        this(documentProto, new ArrayMap<>());
-        for (int i = 0; i < documentProto.getPropertiesCount(); i++) {
-            PropertyProto property = documentProto.getProperties(i);
-            String name = property.getName();
-            if (property.getStringValuesCount() > 0) {
-                String[] values = new String[property.getStringValuesCount()];
-                for (int j = 0; j < values.length; j++) {
-                    values[j] = property.getStringValues(j);
-                }
-                mProperties.put(name, values);
-            } else if (property.getInt64ValuesCount() > 0) {
-                long[] values = new long[property.getInt64ValuesCount()];
-                for (int j = 0; j < values.length; j++) {
-                    values[j] = property.getInt64Values(j);
-                }
-                mProperties.put(property.getName(), values);
-            } else if (property.getDoubleValuesCount() > 0) {
-                double[] values = new double[property.getDoubleValuesCount()];
-                for (int j = 0; j < values.length; j++) {
-                    values[j] = property.getDoubleValues(j);
-                }
-                mProperties.put(property.getName(), values);
-            } else if (property.getBooleanValuesCount() > 0) {
-                boolean[] values = new boolean[property.getBooleanValuesCount()];
-                for (int j = 0; j < values.length; j++) {
-                    values[j] = property.getBooleanValues(j);
-                }
-                mProperties.put(property.getName(), values);
-            } else if (property.getBytesValuesCount() > 0) {
-                byte[][] values = new byte[property.getBytesValuesCount()][];
-                for (int j = 0; j < values.length; j++) {
-                    values[j] = property.getBytesValues(j).toByteArray();
-                }
-                mProperties.put(name, values);
-            } else if (property.getDocumentValuesCount() > 0) {
-                AppSearchDocument[] values =
-                        new AppSearchDocument[property.getDocumentValuesCount()];
-                for (int j = 0; j < values.length; j++) {
-                    values[j] = new AppSearchDocument(property.getDocumentValues(j));
-                }
-                mProperties.put(name, values);
-            } else {
-                throw new IllegalStateException("Unknown type of value: " + name);
-            }
-        }
-    }
-
-    /**
-     * Returns the {@link DocumentProto} of the {@link AppSearchDocument}.
-     *
-     * <p>The {@link DocumentProto} contains {@link AppSearchDocument}'s basic information and all
-     *    properties ordered by keys.
-     * @hide
-     */
-    @NonNull
-    @VisibleForTesting
-    public DocumentProto getProto() {
-        return mProto;
-    }
-
-    /** Returns the URI of the {@link AppSearchDocument}. */
-    @NonNull
-    public String getUri() {
-        return mProto.getUri();
-    }
-
-    /** Returns the schema type of the {@link AppSearchDocument}. */
-    @NonNull
-    public String getSchemaType() {
-        return mProto.getSchema();
-    }
-
-    /**
-     * Returns the creation timestamp in milliseconds of the {@link AppSearchDocument}. Value will
-     * be in the {@link System#currentTimeMillis()} time base.
-     */
-    @CurrentTimeMillisLong
-    public long getCreationTimestampMillis() {
-        return mProto.getCreationTimestampMs();
-    }
-
-    /**
-     * Returns the TTL (Time To Live) of the {@link AppSearchDocument}, in milliseconds.
-     *
-     * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
-     *    until the app is uninstalled.
-     */
-    @DurationMillisLong
-    public long getTtlMillis() {
-        return mProto.getTtlMs();
-    }
-
-    /**
-     * Returns the score of the {@link AppSearchDocument}.
-     *
-     * <p>The score is a query-independent measure of the document's quality, relative to other
-     * {@link AppSearchDocument}s of the same type.
-     *
-     * <p>The default value is 0.
-     */
-    public int getScore() {
-        return mProto.getScore();
-    }
-
-    /**
-     * Retrieve a {@link String} value by key.
-     *
-     * @param key The key to look for.
-     * @return The first {@link String} associated with the given key or {@code null} if there
-     *         is no such key or the value is of a different type.
-     */
-    @Nullable
-    public String getPropertyString(@NonNull String key) {
-        String[] propertyArray = getPropertyStringArray(key);
-        if (ArrayUtils.isEmpty(propertyArray)) {
-            return null;
-        }
-        warnIfSinglePropertyTooLong("String", key, propertyArray.length);
-        return propertyArray[0];
-    }
-
-    /**
-     * Retrieve a {@code long} value by key.
-     *
-     * @param key The key to look for.
-     * @return The first {@code long} associated with the given key or default value {@code 0} if
-     *         there is no such key or the value is of a different type.
-     */
-    public long getPropertyLong(@NonNull String key) {
-        long[] propertyArray = getPropertyLongArray(key);
-        if (ArrayUtils.isEmpty(propertyArray)) {
-            return 0;
-        }
-        warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
-        return propertyArray[0];
-    }
-
-    /**
-     * Retrieve a {@code double} value by key.
-     *
-     * @param key The key to look for.
-     * @return The first {@code double} associated with the given key or default value {@code 0.0}
-     *         if there is no such key or the value is of a different type.
-     */
-    public double getPropertyDouble(@NonNull String key) {
-        double[] propertyArray = getPropertyDoubleArray(key);
-        // TODO(tytytyww): Add support double array to ArraysUtils.isEmpty().
-        if (propertyArray == null || propertyArray.length == 0) {
-            return 0.0;
-        }
-        warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
-        return propertyArray[0];
-    }
-
-    /**
-     * Retrieve a {@code boolean} value by key.
-     *
-     * @param key The key to look for.
-     * @return The first {@code boolean} associated with the given key or default value
-     *         {@code false} if there is no such key or the value is of a different type.
-     */
-    public boolean getPropertyBoolean(@NonNull String key) {
-        boolean[] propertyArray = getPropertyBooleanArray(key);
-        if (ArrayUtils.isEmpty(propertyArray)) {
-            return false;
-        }
-        warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length);
-        return propertyArray[0];
-    }
-
-    /**
-     * Retrieve a {@code byte[]} value by key.
-     *
-     * @param key The key to look for.
-     * @return The first {@code byte[]} associated with the given key or {@code null} if there
-     *         is no such key or the value is of a different type.
-     */
-    @Nullable
-    public byte[] getPropertyBytes(@NonNull String key) {
-        byte[][] propertyArray = getPropertyBytesArray(key);
-        if (ArrayUtils.isEmpty(propertyArray)) {
-            return null;
-        }
-        warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length);
-        return propertyArray[0];
-    }
-
-    /**
-     * Retrieve a {@link AppSearchDocument} value by key.
-     *
-     * @param key The key to look for.
-     * @return The first {@link AppSearchDocument} associated with the given key or {@code null} if
-     *         there is no such key or the value is of a different type.
-     */
-    @Nullable
-    public AppSearchDocument getPropertyDocument(@NonNull String key) {
-        AppSearchDocument[] propertyArray = getPropertyDocumentArray(key);
-        if (ArrayUtils.isEmpty(propertyArray)) {
-            return null;
-        }
-        warnIfSinglePropertyTooLong("Document", key, propertyArray.length);
-        return propertyArray[0];
-    }
-
-    /** Prints a warning to logcat if the given propertyLength is greater than 1. */
-    private static void warnIfSinglePropertyTooLong(
-            @NonNull String propertyType, @NonNull String key, int propertyLength) {
-        if (propertyLength > 1) {
-            Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength
-                    + " elements. Only the first one will be returned from "
-                    + "getProperty" + propertyType + "(). Try getProperty" + propertyType
-                    + "Array().");
-        }
-    }
-
-    /**
-     * Retrieve a repeated {@link String} property by key.
-     *
-     * @param key The key to look for.
-     * @return The {@code String[]} associated with the given key, or {@code null} if no value
-     *         is set or the value is of a different type.
-     */
-    @Nullable
-    public String[] getPropertyStringArray(@NonNull String key) {
-        return getAndCastPropertyArray(key, String[].class);
-    }
-
-    /**
-     * Retrieve a repeated {@code long} property by key.
-     *
-     * @param key The key to look for.
-     * @return The {@code long[]} associated with the given key, or {@code null} if no value is
-     *         set or the value is of a different type.
-     */
-    @Nullable
-    public long[] getPropertyLongArray(@NonNull String key) {
-        return getAndCastPropertyArray(key, long[].class);
-    }
-
-    /**
-     * Retrieve a repeated {@code double} property by key.
-     *
-     * @param key The key to look for.
-     * @return The {@code double[]} associated with the given key, or {@code null} if no value
-     *         is set or the value is of a different type.
-     */
-    @Nullable
-    public double[] getPropertyDoubleArray(@NonNull String key) {
-        return getAndCastPropertyArray(key, double[].class);
-    }
-
-    /**
-     * Retrieve a repeated {@code boolean} property by key.
-     *
-     * @param key The key to look for.
-     * @return The {@code boolean[]} associated with the given key, or {@code null} if no value
-     *         is set or the value is of a different type.
-     */
-    @Nullable
-    public boolean[] getPropertyBooleanArray(@NonNull String key) {
-        return getAndCastPropertyArray(key, boolean[].class);
-    }
-
-    /**
-     * Retrieve a {@code byte[][]} property by key.
-     *
-     * @param key The key to look for.
-     * @return The {@code byte[][]} associated with the given key, or {@code null} if no value
-     *         is set or the value is of a different type.
-     */
-    @Nullable
-    public byte[][] getPropertyBytesArray(@NonNull String key) {
-        return getAndCastPropertyArray(key, byte[][].class);
-    }
-
-    /**
-     * Retrieve a repeated {@link AppSearchDocument} property by key.
-     *
-     * @param key The key to look for.
-     * @return The {@link AppSearchDocument[]} associated with the given key, or {@code null} if no
-     *         value is set or the value is of a different type.
-     */
-    @Nullable
-    public AppSearchDocument[] getPropertyDocumentArray(@NonNull String key) {
-        return getAndCastPropertyArray(key, AppSearchDocument[].class);
-    }
-
-    /**
-     * Gets a repeated property of the given key, and casts it to the given class type, which
-     * must be an array class type.
-     */
-    @Nullable
-    private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
-        Object value = mProperties.get(key);
-        if (value == null) {
-            return null;
-        }
-        try {
-            return tClass.cast(value);
-        } catch (ClassCastException e) {
-            Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e);
-            return null;
-        }
-    }
-
-    @Override
-    public boolean equals(@Nullable Object other) {
-        // Check only proto's equality is sufficient here since all properties in
-        // mProperties are ordered by keys and stored in proto.
-        if (this == other) {
-            return true;
-        }
-        if (!(other instanceof AppSearchDocument)) {
-            return false;
-        }
-        AppSearchDocument otherDocument = (AppSearchDocument) other;
-        return this.mProto.equals(otherDocument.mProto);
-    }
-
-    @Override
-    public int hashCode() {
-        // Hash only proto is sufficient here since all properties in mProperties are ordered by
-        // keys and stored in proto.
-        return mProto.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return mProto.toString();
-    }
-
-    /**
-     * The builder class for {@link AppSearchDocument}.
-     *
-     * @param <BuilderType> Type of subclass who extend this.
-     */
-    public static class Builder<BuilderType extends Builder> {
-
-        private final Map<String, Object> mProperties = new ArrayMap<>();
-        private final DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
-        private final BuilderType mBuilderTypeInstance;
-
-        /**
-         * Creates a new {@link AppSearchDocument.Builder}.
-         *
-         * <p>The URI is a unique string opaque to AppSearch.
-         *
-         * @param uri The uri of {@link AppSearchDocument}.
-         * @param schemaType The schema type of the {@link AppSearchDocument}. The passed-in
-         *       {@code schemaType} must be defined using {@link AppSearchManager#setSchema} prior
-         *       to inserting a document of this {@code schemaType} into the AppSearch index using
-         *       {@link AppSearchManager#putDocuments(List)}. Otherwise, the document will be
-         *       rejected by {@link AppSearchManager#putDocuments(List)}.
-         */
-        public Builder(@NonNull String uri, @NonNull String schemaType) {
-            mBuilderTypeInstance = (BuilderType) this;
-            mProtoBuilder.setUri(uri).setSchema(schemaType).setNamespace(DEFAULT_NAMESPACE);
-            // Set current timestamp for creation timestamp by default.
-            setCreationTimestampMillis(System.currentTimeMillis());
-        }
-
-        /**
-         * Sets the score of the {@link AppSearchDocument}.
-         *
-         * <p>The score is a query-independent measure of the document's quality, relative to
-         * other {@link AppSearchDocument}s of the same type.
-         *
-         * @throws IllegalArgumentException If the provided value is negative.
-         */
-        @NonNull
-        public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
-            if (score < 0) {
-                throw new IllegalArgumentException("Document score cannot be negative.");
-            }
-            mProtoBuilder.setScore(score);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Set the creation timestamp in milliseconds of the {@link AppSearchDocument}. Should be
-         * set using a value obtained from the {@link System#currentTimeMillis()} time base.
-         */
-        @NonNull
-        public BuilderType setCreationTimestampMillis(
-                @CurrentTimeMillisLong long creationTimestampMillis) {
-            mProtoBuilder.setCreationTimestampMs(creationTimestampMillis);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Set the TTL (Time To Live) of the {@link AppSearchDocument}, in milliseconds.
-         *
-         * <p>After this many milliseconds since the {@link #setCreationTimestampMillis(long)}
-         * creation timestamp}, the document is deleted.
-         *
-         * @param ttlMillis A non-negative duration in milliseconds.
-         * @throws IllegalArgumentException If the provided value is negative.
-         */
-        @NonNull
-        public BuilderType setTtlMillis(@DurationMillisLong long ttlMillis) {
-            Preconditions.checkArgumentNonNegative(
-                    ttlMillis, "Document ttlMillis cannot be negative.");
-            mProtoBuilder.setTtlMs(ttlMillis);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Sets one or multiple {@code String} values for a property, replacing its previous
-         * values.
-         *
-         * @param key The key associated with the {@code values}.
-         * @param values The {@code String} values of the property.
-         */
-        @NonNull
-        public BuilderType setProperty(@NonNull String key, @NonNull String... values) {
-            putInPropertyMap(key, values);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Sets one or multiple {@code boolean} values for a property, replacing its previous
-         * values.
-         *
-         * @param key The key associated with the {@code values}.
-         * @param values The {@code boolean} values of the property.
-         */
-        @NonNull
-        public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) {
-            putInPropertyMap(key, values);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Sets one or multiple {@code long} values for a property, replacing its previous
-         * values.
-         *
-         * @param key The key associated with the {@code values}.
-         * @param values The {@code long} values of the property.
-         */
-        @NonNull
-        public BuilderType setProperty(@NonNull String key, @NonNull long... values) {
-            putInPropertyMap(key, values);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Sets one or multiple {@code double} values for a property, replacing its previous
-         * values.
-         *
-         * @param key The key associated with the {@code values}.
-         * @param values The {@code double} values of the property.
-         */
-        @NonNull
-        public BuilderType setProperty(@NonNull String key, @NonNull double... values) {
-            putInPropertyMap(key, values);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
-         *
-         * @param key The key associated with the {@code values}.
-         * @param values The {@code byte[]} of the property.
-         */
-        @NonNull
-        public BuilderType setProperty(@NonNull String key, @NonNull byte[]... values) {
-            putInPropertyMap(key, values);
-            return mBuilderTypeInstance;
-        }
-
-        /**
-         * Sets one or multiple {@link AppSearchDocument} values for a property, replacing its
-         * previous values.
-         *
-         * @param key The key associated with the {@code values}.
-         * @param values The {@link AppSearchDocument} values of the property.
-         */
-        @NonNull
-        public BuilderType setProperty(@NonNull String key, @NonNull AppSearchDocument... values) {
-            putInPropertyMap(key, values);
-            return mBuilderTypeInstance;
-        }
-
-        private void putInPropertyMap(@NonNull String key, @NonNull String[] values)
-                throws IllegalArgumentException {
-            Objects.requireNonNull(key);
-            Objects.requireNonNull(values);
-            validateRepeatedPropertyLength(key, values.length);
-            for (int i = 0; i < values.length; i++) {
-                if (values[i] == null) {
-                    throw new IllegalArgumentException("The String at " + i + " is null.");
-                } else if (values[i].length() > MAX_STRING_LENGTH) {
-                    throw new IllegalArgumentException("The String at " + i + " length is: "
-                            + values[i].length()  + ", which exceeds length limit: "
-                            + MAX_STRING_LENGTH + ".");
-                }
-            }
-            mProperties.put(key, values);
-        }
-
-        private void putInPropertyMap(@NonNull String key, @NonNull boolean[] values) {
-            Objects.requireNonNull(key);
-            Objects.requireNonNull(values);
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.put(key, values);
-        }
-
-        private void putInPropertyMap(@NonNull String key, @NonNull double[] values) {
-            Objects.requireNonNull(key);
-            Objects.requireNonNull(values);
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.put(key, values);
-        }
-
-        private void putInPropertyMap(@NonNull String key, @NonNull long[] values) {
-            Objects.requireNonNull(key);
-            Objects.requireNonNull(values);
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.put(key, values);
-        }
-
-        private void putInPropertyMap(@NonNull String key, @NonNull byte[][] values) {
-            Objects.requireNonNull(key);
-            Objects.requireNonNull(values);
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.put(key, values);
-        }
-
-        private void putInPropertyMap(@NonNull String key, @NonNull AppSearchDocument[] values) {
-            Objects.requireNonNull(key);
-            Objects.requireNonNull(values);
-            for (int i = 0; i < values.length; i++) {
-                if (values[i] == null) {
-                    throw new IllegalArgumentException("The document at " + i + " is null.");
-                }
-            }
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.put(key, values);
-        }
-
-        private static void validateRepeatedPropertyLength(@NonNull String key, int length) {
-            if (length == 0) {
-                throw new IllegalArgumentException("The input array is empty.");
-            } else if (length > MAX_REPEATED_PROPERTY_LENGTH) {
-                throw new IllegalArgumentException(
-                        "Repeated property \"" + key + "\" has length " + length
-                                + ", which exceeds the limit of "
-                                + MAX_REPEATED_PROPERTY_LENGTH);
-            }
-        }
-
-        /** Builds the {@link AppSearchDocument} object. */
-        @NonNull
-        public AppSearchDocument build() {
-            // Build proto by sorting the keys in mProperties to exclude the influence of
-            // order. Therefore documents will generate same proto as long as the contents are
-            // same. Note that the order of repeated fields is still preserved.
-            ArrayList<String> keys = new ArrayList<>(mProperties.keySet());
-            Collections.sort(keys);
-            for (int i = 0; i < keys.size(); i++) {
-                String name = keys.get(i);
-                Object values = mProperties.get(name);
-                PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(name);
-                if (values instanceof boolean[]) {
-                    for (boolean value : (boolean[]) values) {
-                        propertyProto.addBooleanValues(value);
-                    }
-                } else if (values instanceof long[]) {
-                    for (long value : (long[]) values) {
-                        propertyProto.addInt64Values(value);
-                    }
-                } else if (values instanceof double[]) {
-                    for (double value : (double[]) values) {
-                        propertyProto.addDoubleValues(value);
-                    }
-                } else if (values instanceof String[]) {
-                    for (String value : (String[]) values) {
-                        propertyProto.addStringValues(value);
-                    }
-                } else if (values instanceof AppSearchDocument[]) {
-                    for (AppSearchDocument value : (AppSearchDocument[]) values) {
-                        propertyProto.addDocumentValues(value.getProto());
-                    }
-                } else if (values instanceof byte[][]) {
-                    for (byte[] value : (byte[][]) values) {
-                        propertyProto.addBytesValues(ByteString.copyFrom(value));
-                    }
-                } else {
-                    throw new IllegalStateException(
-                            "Property \"" + name + "\" has unsupported value type \""
-                                    + values.getClass().getSimpleName() + "\"");
-                }
-                mProtoBuilder.addProperties(propertyProto);
-            }
-            return new AppSearchDocument(mProtoBuilder.build(), mProperties);
-        }
-    }
-}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java
index b13dd9f..5f2fabe 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchEmail.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,20 +16,26 @@
 
 package android.app.appsearch;
 
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+
 import android.app.appsearch.AppSearchSchema.PropertyConfig;
 
 /**
- * Encapsulates a {@link AppSearchDocument} that represent an email.
+ * Encapsulates a {@link GenericDocument} that represent an email.
  *
- * <p>This class is a higher level implement of {@link AppSearchDocument}.
+ * <p>This class is a higher level implement of {@link GenericDocument}.
  *
  * <p>This class will eventually migrate to Jetpack, where it will become public API.
  *
  * @hide
  */
-public class AppSearchEmail extends AppSearchDocument {
+
+public class AppSearchEmail extends GenericDocument {
+    /** The name of the schema type for {@link AppSearchEmail} documents.*/
+    public static final String SCHEMA_TYPE = "builtin:Email";
+
     private static final String KEY_FROM = "from";
     private static final String KEY_TO = "to";
     private static final String KEY_CC = "cc";
@@ -37,46 +43,43 @@
     private static final String KEY_SUBJECT = "subject";
     private static final String KEY_BODY = "body";
 
-    /** The name of the schema type for {@link AppSearchEmail} documents.*/
-    public static final String SCHEMA_TYPE = "builtin:Email";
-
     public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
-            .addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_FROM)
+            .addProperty(new PropertyConfig.Builder(KEY_FROM)
                     .setDataType(PropertyConfig.DATA_TYPE_STRING)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                     .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
                     .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_TO)
+            ).addProperty(new PropertyConfig.Builder(KEY_TO)
                     .setDataType(PropertyConfig.DATA_TYPE_STRING)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
                     .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
                     .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_CC)
+            ).addProperty(new PropertyConfig.Builder(KEY_CC)
                     .setDataType(PropertyConfig.DATA_TYPE_STRING)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
                     .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
                     .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BCC)
+            ).addProperty(new PropertyConfig.Builder(KEY_BCC)
                     .setDataType(PropertyConfig.DATA_TYPE_STRING)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
                     .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
                     .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_SUBJECT)
+            ).addProperty(new PropertyConfig.Builder(KEY_SUBJECT)
                     .setDataType(PropertyConfig.DATA_TYPE_STRING)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                     .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
                     .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new AppSearchSchema.PropertyConfig.Builder(KEY_BODY)
+            ).addProperty(new PropertyConfig.Builder(KEY_BODY)
                     .setDataType(PropertyConfig.DATA_TYPE_STRING)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                     .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
@@ -87,12 +90,11 @@
 
     /**
      * Creates a new {@link AppSearchEmail} from the contents of an existing
-     * {@link AppSearchDocument}.
+     * {@link GenericDocument}.
      *
-     * @param document The {@link AppSearchDocument} containing the email content.
-     * @hide
+     * @param document The {@link GenericDocument} containing the email content.
      */
-    public AppSearchEmail(@NonNull AppSearchDocument document) {
+    public AppSearchEmail(@NonNull GenericDocument document) {
         super(document);
     }
 
@@ -101,7 +103,6 @@
      *
      * @return Returns the subject of {@link AppSearchEmail} or {@code null} if it's not been set
      *         yet.
-     * @hide
      */
     @Nullable
     public String getFrom() {
@@ -113,7 +114,6 @@
      *
      * @return Returns the destination addresses of {@link AppSearchEmail} or {@code null} if it's
      *         not been set yet.
-     * @hide
      */
     @Nullable
     public String[] getTo() {
@@ -125,7 +125,6 @@
      *
      * @return Returns the CC list of {@link AppSearchEmail} or {@code null} if it's not been set
      *         yet.
-     * @hide
      */
     @Nullable
     public String[] getCc() {
@@ -137,7 +136,6 @@
      *
      * @return Returns the BCC list of {@link AppSearchEmail} or {@code null} if it's not been set
      *         yet.
-     * @hide
      */
     @Nullable
     public String[] getBcc() {
@@ -149,7 +147,6 @@
      *
      * @return Returns the value subject of {@link AppSearchEmail} or {@code null} if it's not been
      *         set yet.
-     * @hide
      */
     @Nullable
     public String getSubject() {
@@ -160,7 +157,6 @@
      * Get the body of {@link AppSearchEmail}.
      *
      * @return Returns the body of {@link AppSearchEmail} or {@code null} if it's not been set yet.
-     * @hide
      */
     @Nullable
     public String getBody() {
@@ -169,14 +165,12 @@
 
     /**
      * The builder class for {@link AppSearchEmail}.
-     * @hide
      */
-    public static class Builder extends AppSearchDocument.Builder<AppSearchEmail.Builder> {
+    public static class Builder extends GenericDocument.Builder<AppSearchEmail.Builder> {
 
         /**
          * Create a new {@link AppSearchEmail.Builder}
          * @param uri The Uri of the Email.
-         * @hide
          */
         public Builder(@NonNull String uri) {
             super(uri, SCHEMA_TYPE);
@@ -184,7 +178,6 @@
 
         /**
          * Set the from address of {@link AppSearchEmail}
-         * @hide
          */
         @NonNull
         public AppSearchEmail.Builder setFrom(@NonNull String from) {
@@ -194,7 +187,6 @@
 
         /**
          * Set the destination address of {@link AppSearchEmail}
-         * @hide
          */
         @NonNull
         public AppSearchEmail.Builder setTo(@NonNull String... to) {
@@ -204,7 +196,6 @@
 
         /**
          * Set the CC list of {@link AppSearchEmail}
-         * @hide
          */
         @NonNull
         public AppSearchEmail.Builder setCc(@NonNull String... cc) {
@@ -214,7 +205,6 @@
 
         /**
          * Set the BCC list of {@link AppSearchEmail}
-         * @hide
          */
         @NonNull
         public AppSearchEmail.Builder setBcc(@NonNull String... bcc) {
@@ -224,7 +214,6 @@
 
         /**
          * Set the subject of {@link AppSearchEmail}
-         * @hide
          */
         @NonNull
         public AppSearchEmail.Builder setSubject(@NonNull String subject) {
@@ -234,7 +223,6 @@
 
         /**
          * Set the body of {@link AppSearchEmail}
-         * @hide
          */
         @NonNull
         public AppSearchEmail.Builder setBody(@NonNull String body) {
@@ -242,11 +230,7 @@
             return this;
         }
 
-        /**
-         * Builds the {@link AppSearchEmail} object.
-         *
-         * @hide
-         */
+        /** Builds the {@link AppSearchEmail} object. */
         @NonNull
         @Override
         public AppSearchEmail build() {
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
index ad51a5c..b38bb05 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java
@@ -23,11 +23,6 @@
 
 import com.android.internal.infra.AndroidFuture;
 
-import com.google.android.icing.proto.DocumentProto;
-import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.StatusProto;
-import com.google.protobuf.InvalidProtocolBufferException;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -78,8 +73,8 @@
      *     <li>Removal of an existing type
      *     <li>Removal of a property from a type
      *     <li>Changing the data type ({@code boolean}, {@code long}, etc.) of an existing property
-     *     <li>For properties of {@code AppSearchDocument} type, changing the schema type of
-     *         {@code AppSearchDocument}s of that property
+     *     <li>For properties of {@code GenericDocument} type, changing the schema type of
+     *         {@code GenericDocument}s of that property
      *     <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an
      *         {@link android.app.appsearch.AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL
      *             OPTIONAL} property into a
@@ -148,31 +143,31 @@
     }
 
     /**
-     * Index {@link AppSearchDocument}s into AppSearch.
+     * Index {@link GenericDocument}s into AppSearch.
      *
      * <p>You should not call this method directly; instead, use the
      * {@code AppSearch#putDocuments()} API provided by JetPack.
      *
-     * <p>Each {@link AppSearchDocument}'s {@code schemaType} field must be set to the name of a
+     * <p>Each {@link GenericDocument}'s {@code schemaType} field must be set to the name of a
      * schema type previously registered via the {@link #setSchema} method.
      *
-     * @param documents {@link AppSearchDocument}s that need to be indexed.
+     * @param documents {@link GenericDocument}s that need to be indexed.
      * @return An {@link AppSearchBatchResult} mapping the document URIs to {@link Void} if they
      *     were successfully indexed, or a {@link Throwable} describing the failure if they could
      *     not be indexed.
      * @hide
      */
     public AppSearchBatchResult<String, Void> putDocuments(
-            @NonNull List<AppSearchDocument> documents) {
+            @NonNull List<GenericDocument> documents) {
         // TODO(b/146386470): Transmit these documents as a RemoteStream instead of sending them in
         // one big list.
-        List<byte[]> documentsBytes = new ArrayList<>(documents.size());
-        for (AppSearchDocument document : documents) {
-            documentsBytes.add(document.getProto().toByteArray());
+        List<Bundle> documentBundles = new ArrayList<>(documents.size());
+        for (GenericDocument document : documents) {
+            documentBundles.add(document.getBundle());
         }
         AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
         try {
-            mService.putDocuments(documentsBytes, future);
+            mService.putDocuments(documentBundles, future);
         } catch (RemoteException e) {
             future.completeExceptionally(e);
         }
@@ -180,19 +175,18 @@
     }
 
     /**
-     * Retrieves {@link AppSearchDocument}s by URI.
+     * Retrieves {@link GenericDocument}s by URI.
      *
      * <p>You should not call this method directly; instead, use the
      * {@code AppSearch#getDocuments()} API provided by JetPack.
      *
      * @param uris URIs of the documents to look up.
      * @return An {@link AppSearchBatchResult} mapping the document URIs to
-     *     {@link AppSearchDocument} values if they were successfully retrieved, a {@code null}
+     *     {@link GenericDocument} values if they were successfully retrieved, a {@code null}
      *     failure if they were not found, or a {@link Throwable} failure describing the problem if
      *     an error occurred.
      */
-    public AppSearchBatchResult<String, AppSearchDocument> getDocuments(
-            @NonNull List<String> uris) {
+    public AppSearchBatchResult<String, GenericDocument> getDocuments(@NonNull List<String> uris) {
         // TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
         //     them in one big list.
         AndroidFuture<AppSearchBatchResult> future = new AndroidFuture<>();
@@ -202,43 +196,35 @@
             future.completeExceptionally(e);
         }
 
-        // Deserialize the protos into Document objects
-        AppSearchBatchResult<String, byte[]> protoResults = getFutureOrThrow(future);
-        AppSearchBatchResult.Builder<String, AppSearchDocument> documentResultBuilder =
+        // Translate from document bundles to GenericDocument instances
+        AppSearchBatchResult<String, Bundle> bundleResult = getFutureOrThrow(future);
+        AppSearchBatchResult.Builder<String, GenericDocument> documentResultBuilder =
                 new AppSearchBatchResult.Builder<>();
 
         // Translate successful results
-        for (Map.Entry<String, byte[]> protoResult : protoResults.getSuccesses().entrySet()) {
-            DocumentProto documentProto;
+        for (Map.Entry<String, Bundle> bundleEntry : bundleResult.getSuccesses().entrySet()) {
+            GenericDocument document;
             try {
-                documentProto = DocumentProto.parseFrom(protoResult.getValue());
-            } catch (InvalidProtocolBufferException e) {
-                documentResultBuilder.setFailure(
-                        protoResult.getKey(), AppSearchResult.RESULT_IO_ERROR, e.getMessage());
-                continue;
-            }
-            AppSearchDocument document;
-            try {
-                document = new AppSearchDocument(documentProto);
+                document = new GenericDocument(bundleEntry.getValue());
             } catch (Throwable t) {
                 // These documents went through validation, so how could this fail? We must have
                 // done something wrong.
                 documentResultBuilder.setFailure(
-                        protoResult.getKey(),
+                        bundleEntry.getKey(),
                         AppSearchResult.RESULT_INTERNAL_ERROR,
                         t.getMessage());
                 continue;
             }
-            documentResultBuilder.setSuccess(protoResult.getKey(), document);
+            documentResultBuilder.setSuccess(bundleEntry.getKey(), document);
         }
 
         // Translate failed results
-        for (Map.Entry<String, AppSearchResult<byte[]>> protoResult :
-                protoResults.getFailures().entrySet()) {
+        for (Map.Entry<String, AppSearchResult<Bundle>> bundleEntry :
+                bundleResult.getFailures().entrySet()) {
             documentResultBuilder.setFailure(
-                    protoResult.getKey(),
-                    protoResult.getValue().getResultCode(),
-                    protoResult.getValue().getErrorMessage());
+                    bundleEntry.getKey(),
+                    bundleEntry.getValue().getResultCode(),
+                    bundleEntry.getValue().getErrorMessage());
         }
 
         return documentResultBuilder.build();
@@ -287,43 +273,29 @@
      * @hide
      */
     @NonNull
-    public AppSearchResult<SearchResults> query(
+    public AppSearchResult<List<SearchResult>> query(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         // TODO(b/146386470): Transmit the result documents as a RemoteStream instead of sending
         //     them in one big list.
-        AndroidFuture<AppSearchResult> searchResultFuture = new AndroidFuture<>();
+        AndroidFuture<AppSearchResult> searchResultsFuture = new AndroidFuture<>();
         try {
-            mService.query(queryExpression, searchSpec.getBundle(), searchResultFuture);
+            mService.query(queryExpression, searchSpec.getBundle(), searchResultsFuture);
         } catch (RemoteException e) {
-            searchResultFuture.completeExceptionally(e);
+            searchResultsFuture.completeExceptionally(e);
         }
 
-        // Deserialize the protos into Document objects
-        AppSearchResult<byte[]> searchResultBytes = getFutureOrThrow(searchResultFuture);
-        if (!searchResultBytes.isSuccess()) {
+        // Translate the list of Bundle into a list of SearchResult
+        AppSearchResult<SearchResults> searchResultsResult = getFutureOrThrow(searchResultsFuture);
+        if (!searchResultsResult.isSuccess()) {
             return AppSearchResult.newFailedResult(
-                    searchResultBytes.getResultCode(), searchResultBytes.getErrorMessage());
+                    searchResultsResult.getResultCode(), searchResultsResult.getErrorMessage());
         }
-        SearchResultProto searchResultProto;
-        try {
-            searchResultProto = SearchResultProto.parseFrom(searchResultBytes.getResultValue());
-        } catch (InvalidProtocolBufferException e) {
-            return AppSearchResult.newFailedResult(
-                    AppSearchResult.RESULT_INTERNAL_ERROR, e.getMessage());
-        }
-        if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
-            // This should never happen; AppSearchManagerService should catch failed searchResults
-            // entries and transmit them as a failed AppSearchResult.
-            return AppSearchResult.newFailedResult(
-                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                    searchResultProto.getStatus().getMessage());
-        }
-
-        return AppSearchResult.newSuccessfulResult(new SearchResults(searchResultProto));
+        SearchResults searchResults = searchResultsResult.getResultValue();
+        return AppSearchResult.newSuccessfulResult(searchResults.mResults);
     }
 
     /**
-     * Deletes {@link AppSearchDocument}s by URI.
+     * Deletes {@link GenericDocument}s by URI.
      *
      * <p>You should not call this method directly; instead, use the {@code AppSearch#delete()} API
      * provided by JetPack.
diff --git a/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java b/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java
new file mode 100644
index 0000000..9fe2c67
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/GenericDocument.java
@@ -0,0 +1,923 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.util.Log;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import android.app.appsearch.exceptions.AppSearchException;
+import com.android.internal.util.Preconditions;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Represents a document unit.
+ *
+ * <p>Documents are constructed via {@link GenericDocument.Builder}.
+ * @hide
+ */
+public class GenericDocument {
+    private static final String TAG = "GenericDocument";
+
+    /** The default empty namespace.*/
+    public static final String DEFAULT_NAMESPACE = "";
+
+    /**
+     * The maximum number of elements in a repeatable field. Will reject the request if exceed
+     * this limit.
+     */
+    private static final int MAX_REPEATED_PROPERTY_LENGTH = 100;
+
+    /**
+     * The maximum {@link String#length} of a {@link String} field. Will reject the request if
+     * {@link String}s longer than this.
+     */
+    private static final int MAX_STRING_LENGTH = 20_000;
+
+    /** The maximum number of indexed properties a document can have. */
+    private static final int MAX_INDEXED_PROPERTIES = 16;
+
+    /** The default score of document. */
+    private static final int DEFAULT_SCORE = 0;
+
+    /** The default time-to-live in millisecond of a document, which is infinity. */
+    private static final long DEFAULT_TTL_MILLIS = 0L;
+
+    /** @hide */
+    
+    public static final String PROPERTIES_FIELD = "properties";
+
+    /** @hide */
+    
+    public static final String BYTE_ARRAY_FIELD = "byteArray";
+
+    static final String SCHEMA_TYPE_FIELD = "schemaType";
+    static final String URI_FIELD = "uri";
+    static final String SCORE_FIELD = "score";
+    static final String TTL_MILLIS_FIELD = "ttlMillis";
+    static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
+    static final String NAMESPACE_FIELD = "namespace";
+
+    /**
+     * The maximum number of indexed properties a document can have.
+     *
+     * <p>Indexed properties are properties where the
+     * {@link android.app.appsearch.annotation.AppSearchDocument.Property#indexingType} constant is
+     * anything other than {@link
+     * android.app.appsearch.AppSearchSchema.PropertyConfig.IndexingType#INDEXING_TYPE_NONE}.
+     */
+    public static int getMaxIndexedProperties() {
+        return MAX_INDEXED_PROPERTIES;
+    }
+
+    /** Contains {@link GenericDocument} basic information (uri, schemaType etc).*/
+    @NonNull
+    final Bundle mBundle;
+
+    /** Contains all properties in {@link GenericDocument} to support getting properties via keys.*/
+    @NonNull
+    private final Bundle mProperties;
+
+    @NonNull
+    private final String mUri;
+    @NonNull
+    private final String mSchemaType;
+    private final long mCreationTimestampMillis;
+    @Nullable
+    private Integer mHashCode;
+
+    /**
+     * Rebuilds a {@link GenericDocument} by the a bundle.
+     * @param bundle Contains {@link GenericDocument} basic information (uri, schemaType etc) and
+     *               a properties bundle contains all properties in {@link GenericDocument} to
+     *               support getting properties via keys.
+     * @hide
+     */
+    
+    public GenericDocument(@NonNull Bundle bundle) {
+        Preconditions.checkNotNull(bundle);
+        mBundle = bundle;
+        mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD));
+        mUri = Preconditions.checkNotNull(mBundle.getString(URI_FIELD));
+        mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
+        mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD,
+                System.currentTimeMillis());
+    }
+
+    /**
+     * Creates a new {@link GenericDocument} from an existing instance.
+     *
+     * <p>This method should be only used by constructor of a subclass.
+     */
+    protected GenericDocument(@NonNull GenericDocument document) {
+        this(document.mBundle);
+    }
+
+    /**
+     * Returns the {@link Bundle} populated by this builder.
+     * @hide
+     */
+    
+    @NonNull
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /** Returns the URI of the {@link GenericDocument}. */
+    @NonNull
+    public String getUri() {
+        return mUri;
+    }
+
+    /** Returns the namespace of the {@link GenericDocument}. */
+    @NonNull
+    public String getNamespace() {
+        return mBundle.getString(NAMESPACE_FIELD, DEFAULT_NAMESPACE);
+    }
+
+    /** Returns the schema type of the {@link GenericDocument}. */
+    @NonNull
+    public String getSchemaType() {
+        return mSchemaType;
+    }
+
+    /** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */
+    public long getCreationTimestampMillis() {
+        return mCreationTimestampMillis;
+    }
+
+    /**
+     * Returns the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
+     *
+     * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
+     *    until the app is uninstalled.
+     */
+    public long getTtlMillis() {
+        return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
+    }
+
+    /**
+     * Returns the score of the {@link GenericDocument}.
+     *
+     * <p>The score is a query-independent measure of the document's quality, relative to other
+     * {@link GenericDocument}s of the same type.
+     *
+     * <p>The default value is 0.
+     */
+    public int getScore() {
+        return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);
+    }
+
+    /**
+     * Retrieves a {@link String} value by key.
+     *
+     * @param key The key to look for.
+     * @return The first {@link String} associated with the given key or {@code null} if there
+     *         is no such key or the value is of a different type.
+     */
+    @Nullable
+    public String getPropertyString(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        String[] propertyArray = getPropertyStringArray(key);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return null;
+        }
+        warnIfSinglePropertyTooLong("String", key, propertyArray.length);
+        return propertyArray[0];
+    }
+
+    /**
+     * Retrieves a {@code long} value by key.
+     *
+     * @param key The key to look for.
+     * @return The first {@code long} associated with the given key or default value {@code 0} if
+     *         there is no such key or the value is of a different type.
+     */
+    public long getPropertyLong(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        long[] propertyArray = getPropertyLongArray(key);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return 0;
+        }
+        warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
+        return propertyArray[0];
+    }
+
+    /**
+     * Retrieves a {@code double} value by key.
+     *
+     * @param key The key to look for.
+     * @return The first {@code double} associated with the given key or default value {@code 0.0}
+     *         if there is no such key or the value is of a different type.
+     */
+    public double getPropertyDouble(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        double[] propertyArray = getPropertyDoubleArray(key);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return 0.0;
+        }
+        warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
+        return propertyArray[0];
+    }
+
+    /**
+     * Retrieves a {@code boolean} value by key.
+     *
+     * @param key The key to look for.
+     * @return The first {@code boolean} associated with the given key or default value
+     *         {@code false} if there is no such key or the value is of a different type.
+     */
+    public boolean getPropertyBoolean(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        boolean[] propertyArray = getPropertyBooleanArray(key);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return false;
+        }
+        warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length);
+        return propertyArray[0];
+    }
+
+    /**
+     * Retrieves a {@code byte[]} value by key.
+     *
+     * @param key The key to look for.
+     * @return The first {@code byte[]} associated with the given key or {@code null} if there
+     *         is no such key or the value is of a different type.
+     */
+    @Nullable
+    public byte[] getPropertyBytes(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        byte[][] propertyArray = getPropertyBytesArray(key);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return null;
+        }
+        warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length);
+        return propertyArray[0];
+    }
+
+    /**
+     * Retrieves a {@link GenericDocument} value by key.
+     *
+     * @param key The key to look for.
+     * @return The first {@link GenericDocument} associated with the given key or {@code null} if
+     *         there is no such key or the value is of a different type.
+     */
+    @Nullable
+    public GenericDocument getPropertyDocument(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        GenericDocument[] propertyArray = getPropertyDocumentArray(key);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return null;
+        }
+        warnIfSinglePropertyTooLong("Document", key, propertyArray.length);
+        return propertyArray[0];
+    }
+
+    /** Prints a warning to logcat if the given propertyLength is greater than 1. */
+    private static void warnIfSinglePropertyTooLong(
+            @NonNull String propertyType, @NonNull String key, int propertyLength) {
+        if (propertyLength > 1) {
+            Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength
+                    + " elements. Only the first one will be returned from "
+                    + "getProperty" + propertyType + "(). Try getProperty" + propertyType
+                    + "Array().");
+        }
+    }
+
+    /**
+     * Retrieves a repeated {@code String} property by key.
+     *
+     * @param key The key to look for.
+     * @return The {@code String[]} associated with the given key, or {@code null} if no value
+     *         is set or the value is of a different type.
+     */
+    @Nullable
+    public String[] getPropertyStringArray(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        return getAndCastPropertyArray(key, String[].class);
+    }
+
+    /**
+     * Retrieves a repeated {@link String} property by key.
+     *
+     * @param key The key to look for.
+     * @return The {@code long[]} associated with the given key, or {@code null} if no value is
+     *         set or the value is of a different type.
+     */
+    @Nullable
+    public long[] getPropertyLongArray(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        return getAndCastPropertyArray(key, long[].class);
+    }
+
+    /**
+     * Retrieves a repeated {@code double} property by key.
+     *
+     * @param key The key to look for.
+     * @return The {@code double[]} associated with the given key, or {@code null} if no value
+     *         is set or the value is of a different type.
+     */
+    @Nullable
+    public double[] getPropertyDoubleArray(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        return getAndCastPropertyArray(key, double[].class);
+    }
+
+    /**
+     * Retrieves a repeated {@code boolean} property by key.
+     *
+     * @param key The key to look for.
+     * @return The {@code boolean[]} associated with the given key, or {@code null} if no value
+     *         is set or the value is of a different type.
+     */
+    @Nullable
+    public boolean[] getPropertyBooleanArray(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        return getAndCastPropertyArray(key, boolean[].class);
+    }
+
+    /**
+     * Retrieves a {@code byte[][]} property by key.
+     *
+     * @param key The key to look for.
+     * @return The {@code byte[][]} associated with the given key, or {@code null} if no value
+     *         is set or the value is of a different type.
+     */
+    @SuppressLint("ArrayReturn")
+    @Nullable
+    @SuppressWarnings("unchecked")
+    public byte[][] getPropertyBytesArray(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        ArrayList<Bundle> bundles = getAndCastPropertyArray(key, ArrayList.class);
+        if (bundles == null || bundles.size() == 0) {
+            return null;
+        }
+        byte[][] bytes = new byte[bundles.size()][];
+        for (int i = 0; i < bundles.size(); i++) {
+            Bundle bundle = bundles.get(i);
+            if (bundle == null) {
+                Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key);
+                continue;
+            }
+            byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);
+            if (innerBytes == null) {
+                Log.e(TAG, "The bundle at " + i + " contains a null byte[].");
+                continue;
+            }
+            bytes[i] = innerBytes;
+        }
+        return bytes;
+    }
+
+    /**
+     * Retrieves a repeated {@link GenericDocument} property by key.
+     *
+     * @param key The key to look for.
+     * @return The {@link GenericDocument}[] associated with the given key, or {@code null} if no
+     *         value is set or the value is of a different type.
+     */
+    @SuppressLint("ArrayReturn")
+    @Nullable
+    public GenericDocument[] getPropertyDocumentArray(@NonNull String key) {
+        Preconditions.checkNotNull(key);
+        Bundle[] bundles = getAndCastPropertyArray(key, Bundle[].class);
+        if (bundles == null || bundles.length == 0) {
+            return null;
+        }
+        GenericDocument[] documents = new GenericDocument[bundles.length];
+        for (int i = 0; i < bundles.length; i++) {
+            if (bundles[i] == null) {
+                Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key);
+                continue;
+            }
+            documents[i] = new GenericDocument(bundles[i]);
+        }
+        return documents;
+    }
+
+    /**
+     * Gets a repeated property of the given key, and casts it to the given class type, which
+     * must be an array class type.
+     */
+    @Nullable
+    private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
+        Object value = mProperties.get(key);
+        if (value == null) {
+            return null;
+        }
+        try {
+            return tClass.cast(value);
+        } catch (ClassCastException e) {
+            Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e);
+            return null;
+        }
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        // Check only proto's equality is sufficient here since all properties in
+        // mProperties are ordered by keys and stored in proto.
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof GenericDocument)) {
+            return false;
+        }
+        GenericDocument otherDocument = (GenericDocument) other;
+        return bundleEquals(this.mBundle, otherDocument.mBundle);
+    }
+
+    /**
+     * Deeply checks two bundle is equally or not.
+     * <p> Two bundle will be considered equally if they contains same content.
+     */
+    @SuppressWarnings("unchecked")
+    private static boolean bundleEquals(Bundle one, Bundle two) {
+        if (one.size() != two.size()) {
+            return false;
+        }
+        Set<String> keySetOne = one.keySet();
+        Object valueOne;
+        Object valueTwo;
+        // Bundle inherit its equals() from Object.java, which only compare their memory address.
+        // We should iterate all keys and check their presents and values in both bundle.
+        for (String key : keySetOne) {
+            valueOne = one.get(key);
+            valueTwo = two.get(key);
+            if (valueOne instanceof Bundle
+                    && valueTwo instanceof Bundle
+                    && !bundleEquals((Bundle) valueOne, (Bundle) valueTwo)) {
+                return false;
+            } else if (valueOne == null && (valueTwo != null || !two.containsKey(key))) {
+                // If we call bundle.get(key) when the 'key' doesn't actually exist in the
+                // bundle, we'll get  back a null. So make sure that both values are null and
+                // both keys exist in the bundle.
+                return false;
+            } else if (valueOne instanceof boolean[]) {
+                if (!(valueTwo instanceof boolean[])
+                        || !Arrays.equals((boolean[]) valueOne, (boolean[]) valueTwo)) {
+                    return false;
+                }
+            } else if (valueOne instanceof long[]) {
+                if (!(valueTwo instanceof long[])
+                        || !Arrays.equals((long[]) valueOne, (long[]) valueTwo)) {
+                    return false;
+                }
+            } else if (valueOne instanceof double[]) {
+                if (!(valueTwo instanceof double[])
+                        || !Arrays.equals((double[]) valueOne, (double[]) valueTwo)) {
+                    return false;
+                }
+            } else if (valueOne instanceof Bundle[]) {
+                if (!(valueTwo instanceof Bundle[])) {
+                    return false;
+                }
+                Bundle[] bundlesOne = (Bundle[]) valueOne;
+                Bundle[] bundlesTwo = (Bundle[]) valueTwo;
+                if (bundlesOne.length != bundlesTwo.length) {
+                    return false;
+                }
+                for (int i = 0; i < bundlesOne.length; i++) {
+                    if (!bundleEquals(bundlesOne[i], bundlesTwo[i])) {
+                        return false;
+                    }
+                }
+            } else if (valueOne instanceof ArrayList) {
+                if (!(valueTwo instanceof ArrayList)) {
+                    return false;
+                }
+                ArrayList<Bundle> bundlesOne = (ArrayList<Bundle>) valueOne;
+                ArrayList<Bundle> bundlesTwo = (ArrayList<Bundle>) valueTwo;
+                if (bundlesOne.size() != bundlesTwo.size()) {
+                    return false;
+                }
+                for (int i = 0; i < bundlesOne.size(); i++) {
+                    if (!bundleEquals(bundlesOne.get(i), bundlesTwo.get(i))) {
+                        return false;
+                    }
+                }
+            } else if (valueOne instanceof Object[]) {
+                if (!(valueTwo instanceof Object[])
+                        || !Arrays.equals((Object[]) valueOne, (Object[]) valueTwo)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            mHashCode = bundleHashCode(mBundle);
+        }
+        return mHashCode;
+    }
+
+    /**
+     * Calculates the hash code for a bundle.
+     * <p> The hash code is only effected by the content in the bundle. Bundles will get
+     * consistent hash code if they have same content.
+     */
+    @SuppressWarnings("unchecked")
+    private static int bundleHashCode(Bundle bundle) {
+        int[] hashCodes = new int[bundle.size()];
+        int i = 0;
+        // Bundle inherit its hashCode() from Object.java, which only relative to their memory
+        // address. Bundle doesn't have an order, so we should iterate all keys and combine
+        // their value's hashcode into an array. And use the hashcode of the array to be
+        // the hashcode of the bundle.
+        for (String key : bundle.keySet()) {
+            Object value = bundle.get(key);
+            if (value instanceof boolean[]) {
+                hashCodes[i++] = Arrays.hashCode((boolean[]) value);
+            } else if (value instanceof long[]) {
+                hashCodes[i++] = Arrays.hashCode((long[]) value);
+            } else if (value instanceof double[]) {
+                hashCodes[i++] = Arrays.hashCode((double[]) value);
+            } else if (value instanceof String[]) {
+                hashCodes[i++] = Arrays.hashCode((Object[]) value);
+            } else if (value instanceof Bundle) {
+                hashCodes[i++] = bundleHashCode((Bundle) value);
+            } else if (value instanceof Bundle[]) {
+                Bundle[] bundles = (Bundle[]) value;
+                int[] innerHashCodes = new int[bundles.length];
+                for (int j = 0; j < innerHashCodes.length; j++) {
+                    innerHashCodes[j] = bundleHashCode(bundles[j]);
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else if (value instanceof ArrayList) {
+                ArrayList<Bundle> bundles = (ArrayList<Bundle>) value;
+                int[] innerHashCodes = new int[bundles.size()];
+                for (int j = 0; j < innerHashCodes.length; j++) {
+                    innerHashCodes[j] = bundleHashCode(bundles.get(j));
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else {
+                hashCodes[i++] = value.hashCode();
+            }
+        }
+        return Arrays.hashCode(hashCodes);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return bundleToString(mBundle).toString();
+    }
+
+    @SuppressWarnings("unchecked")
+    private static StringBuilder bundleToString(Bundle bundle) {
+        StringBuilder stringBuilder = new StringBuilder();
+        try {
+            final Set<String> keySet = bundle.keySet();
+            String[] keys = keySet.toArray(new String[0]);
+            // Sort keys to make output deterministic. We need a custom comparator to handle
+            // nulls (arbitrarily putting them first, similar to Comparator.nullsFirst, which is
+            // only available since N).
+            Arrays.sort(
+                    keys,
+                    (@Nullable String s1, @Nullable String s2) -> {
+                        if (s1 == null) {
+                            return s2 == null ? 0 : -1;
+                        } else if (s2 == null) {
+                            return 1;
+                        } else {
+                            return s1.compareTo(s2);
+                        }
+                    });
+            for (String key : keys) {
+                stringBuilder.append("{ key: '").append(key).append("' value: ");
+                Object valueObject = bundle.get(key);
+                if (valueObject == null) {
+                    stringBuilder.append("<null>");
+                } else if (valueObject instanceof Bundle) {
+                    stringBuilder.append(bundleToString((Bundle) valueObject));
+                } else if (valueObject.getClass().isArray()) {
+                    stringBuilder.append("[ ");
+                    for (int i = 0; i < Array.getLength(valueObject); i++) {
+                        Object element = Array.get(valueObject, i);
+                        stringBuilder.append("'");
+                        if (element instanceof Bundle) {
+                            stringBuilder.append(bundleToString((Bundle) element));
+                        } else {
+                            stringBuilder.append(Array.get(valueObject, i));
+                        }
+                        stringBuilder.append("' ");
+                    }
+                    stringBuilder.append("]");
+                } else if (valueObject instanceof ArrayList) {
+                    for (Bundle innerBundle : (ArrayList<Bundle>) valueObject) {
+                        stringBuilder.append(bundleToString(innerBundle));
+                    }
+                } else {
+                    stringBuilder.append(valueObject.toString());
+                }
+                stringBuilder.append(" } ");
+            }
+        } catch (RuntimeException e) {
+            // Catch any exceptions here since corrupt Bundles can throw different types of
+            // exceptions (e.g. b/38445840 & b/68937025).
+            stringBuilder.append("<error>");
+        }
+        return stringBuilder;
+    }
+
+    /**
+     * The builder class for {@link GenericDocument}.
+     *
+     * @param <BuilderType> Type of subclass who extend this.
+     */
+    public static class Builder<BuilderType extends Builder> {
+
+        private final Bundle mProperties = new Bundle();
+        private final Bundle mBundle = new Bundle();
+        private final BuilderType mBuilderTypeInstance;
+        private boolean mBuilt = false;
+
+        /**
+         * Create a new {@link GenericDocument.Builder}.
+         *
+         * @param uri The uri of {@link GenericDocument}.
+         * @param schemaType The schema type of the {@link GenericDocument}. The passed-in
+         *        {@code schemaType} must be defined using {@code AppSearchManager#setSchema} prior
+         *        to inserting a document of this {@code schemaType} into the AppSearch index using
+         *        {@code AppSearchManager#putDocuments}. Otherwise, the document will be
+         *        rejected by {@code AppSearchManager#putDocuments}.
+         */
+        //TODO(b/157082794) Linkify AppSearchManager once that API is public.
+        @SuppressWarnings("unchecked")
+        public Builder(@NonNull String uri, @NonNull String schemaType) {
+            Preconditions.checkNotNull(uri);
+            Preconditions.checkNotNull(schemaType);
+            mBuilderTypeInstance = (BuilderType) this;
+            mBundle.putString(GenericDocument.URI_FIELD, uri);
+            mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
+            mBundle.putString(GenericDocument.NAMESPACE_FIELD, DEFAULT_NAMESPACE);
+            // Set current timestamp for creation timestamp by default.
+            mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
+                    System.currentTimeMillis());
+            mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
+            mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE);
+            mBundle.putBundle(PROPERTIES_FIELD, mProperties);
+        }
+
+        /**
+         * Set the app-defined namespace this Document resides in. No special values  are
+         * reserved or understood by the infrastructure. URIs are unique within a namespace. The
+         * number of namespaces per app should be kept small for efficiency reasons.
+         */
+        @NonNull
+        public BuilderType setNamespace(@NonNull String namespace) {
+            mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets the score of the {@link GenericDocument}.
+         *
+         * <p>The score is a query-independent measure of the document's quality, relative to
+         * other {@link GenericDocument}s of the same type.
+         *
+         * @throws IllegalArgumentException If the provided value is negative.
+         */
+        @NonNull
+        public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            if (score < 0) {
+                throw new IllegalArgumentException("Document score cannot be negative.");
+            }
+            mBundle.putInt(GenericDocument.SCORE_FIELD, score);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Set the creation timestamp in milliseconds of the {@link GenericDocument}. Should be
+         * set using a value obtained from the {@link System#currentTimeMillis()} time base.
+         */
+        @NonNull
+        public BuilderType setCreationTimestampMillis(long creationTimestampMillis) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
+                    creationTimestampMillis);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Set the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
+         *
+         * <p>After this many milliseconds since the {@link #setCreationTimestampMillis creation
+         * timestamp}, the document is deleted.
+         *
+         * @param ttlMillis A non-negative duration in milliseconds.
+         * @throws IllegalArgumentException If the provided value is negative.
+         */
+        @NonNull
+        public BuilderType setTtlMillis(long ttlMillis) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            if (ttlMillis < 0) {
+                throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
+            }
+            mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets one or multiple {@code String} values for a property, replacing its previous
+         * values.
+         *
+         * @param key The key associated with the {@code values}.
+         * @param values The {@code String} values of the property.
+         */
+        @NonNull
+        public BuilderType setProperty(@NonNull String key, @NonNull String... values) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(values);
+            putInPropertyBundle(key, values);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets one or multiple {@code boolean} values for a property, replacing its previous
+         * values.
+         *
+         * @param key The key associated with the {@code values}.
+         * @param values The {@code boolean} values of the property.
+         */
+        @NonNull
+        public BuilderType setProperty(@NonNull String key, @NonNull boolean... values) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(values);
+            putInPropertyBundle(key, values);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets one or multiple {@code long} values for a property, replacing its previous
+         * values.
+         *
+         * @param key The key associated with the {@code values}.
+         * @param values The {@code long} values of the property.
+         */
+        @NonNull
+        public BuilderType setProperty(@NonNull String key, @NonNull long... values) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(values);
+            putInPropertyBundle(key, values);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets one or multiple {@code double} values for a property, replacing its previous
+         * values.
+         *
+         * @param key The key associated with the {@code values}.
+         * @param values The {@code double} values of the property.
+         */
+        @NonNull
+        public BuilderType setProperty(@NonNull String key, @NonNull double... values) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(values);
+            putInPropertyBundle(key, values);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
+         *
+         * @param key The key associated with the {@code values}.
+         * @param values The {@code byte[]} of the property.
+         */
+        @NonNull
+        public BuilderType setProperty(@NonNull String key, @NonNull byte[]... values) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(values);
+            putInPropertyBundle(key, values);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets one or multiple {@link GenericDocument} values for a property, replacing its
+         * previous values.
+         *
+         * @param key The key associated with the {@code values}.
+         * @param values The {@link GenericDocument} values of the property.
+         */
+        @NonNull
+        public BuilderType setProperty(@NonNull String key, @NonNull GenericDocument... values) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(values);
+            putInPropertyBundle(key, values);
+            return mBuilderTypeInstance;
+        }
+
+        private void putInPropertyBundle(@NonNull String key, @NonNull String[] values)
+                throws IllegalArgumentException {
+            validateRepeatedPropertyLength(key, values.length);
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException("The String at " + i + " is null.");
+                } else if (values[i].length() > MAX_STRING_LENGTH) {
+                    throw new IllegalArgumentException("The String at " + i + " length is: "
+                            + values[i].length()  + ", which exceeds length limit: "
+                            + MAX_STRING_LENGTH + ".");
+                }
+            }
+            mProperties.putStringArray(key, values);
+        }
+
+        private void putInPropertyBundle(@NonNull String key, @NonNull boolean[] values) {
+            validateRepeatedPropertyLength(key, values.length);
+            mProperties.putBooleanArray(key, values);
+        }
+
+        private void putInPropertyBundle(@NonNull String key, @NonNull double[] values) {
+            validateRepeatedPropertyLength(key, values.length);
+            mProperties.putDoubleArray(key, values);
+        }
+
+        private void putInPropertyBundle(@NonNull String key, @NonNull long[] values) {
+            validateRepeatedPropertyLength(key, values.length);
+            mProperties.putLongArray(key, values);
+        }
+
+        /**
+         * Converts and saves a byte[][] into {@link #mProperties}.
+         *
+         * <p>Bundle doesn't support for two dimension array byte[][], we are converting byte[][]
+         * into ArrayList<Bundle>, and each elements will contain a one dimension byte[].
+         */
+        private void putInPropertyBundle(@NonNull String key, @NonNull byte[][] values) {
+            validateRepeatedPropertyLength(key, values.length);
+            ArrayList<Bundle> bundles = new ArrayList<>(values.length);
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException("The byte[] at " + i + " is null.");
+                }
+                Bundle bundle = new Bundle();
+                bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);
+                bundles.add(bundle);
+            }
+            mProperties.putParcelableArrayList(key, bundles);
+        }
+
+        private void putInPropertyBundle(@NonNull String key, @NonNull GenericDocument[] values) {
+            validateRepeatedPropertyLength(key, values.length);
+            Bundle[] documentBundles = new Bundle[values.length];
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException("The document at " + i + " is null.");
+                }
+                documentBundles[i] = values[i].mBundle;
+            }
+            mProperties.putParcelableArray(key, documentBundles);
+        }
+
+        private static void validateRepeatedPropertyLength(@NonNull String key, int length) {
+            if (length == 0) {
+                throw new IllegalArgumentException("The input array is empty.");
+            } else if (length > MAX_REPEATED_PROPERTY_LENGTH) {
+                throw new IllegalArgumentException(
+                        "Repeated property \"" + key + "\" has length " + length
+                                + ", which exceeds the limit of "
+                                + MAX_REPEATED_PROPERTY_LENGTH);
+            }
+        }
+
+        /** Builds the {@link GenericDocument} object. */
+        @NonNull
+        public GenericDocument build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBuilt = true;
+            return new GenericDocument(mBundle);
+        }
+    }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
index c710a29..8e18346 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
+++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl
@@ -21,6 +21,7 @@
 
 parcelable AppSearchResult;
 parcelable AppSearchBatchResult;
+parcelable SearchResults;
 
 /** {@hide} */
 interface IAppSearchManager {
@@ -41,7 +42,7 @@
     /**
      * Inserts documents into the index.
      *
-     * @param documentsBytes {@link List}&lt;byte[]&gt; of serialized DocumentProtos.
+     * @param documentBundes List of GenericDocument bundles.
      * @param callback
      *     {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;&gt;.
      *     If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
@@ -49,18 +50,19 @@
      *     {@link AppSearchBatchResult}&lt;{@link String}, {@link Void}&gt;
      *     where the keys are document URIs, and the values are {@code null}.
      */
-    void putDocuments(in List documentsBytes, in AndroidFuture<AppSearchBatchResult> callback);
+    void putDocuments(
+        in List<Bundle> documentBundles, in AndroidFuture<AppSearchBatchResult> callback);
 
     /**
      * Retrieves documents from the index.
      *
      * @param uris The URIs of the documents to retrieve
      * @param callback
-     *     {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link byte[]}&gt;&gt;.
+     *     {@link AndroidFuture}&lt;{@link AppSearchBatchResult}&lt;{@link String}, {@link Bundle}&gt;&gt;.
      *     If the call fails to start, {@code callback} will be completed exceptionally. Otherwise,
      *     {@code callback} will be completed with an
-     *     {@link AppSearchBatchResult}&lt;{@link String}, {@link byte[]}&gt;
-     *     where the keys are document URIs, and the values are serialized Document protos.
+     *     {@link AppSearchBatchResult}&lt;{@link String}, {@link Bundle}&gt;
+     *     where the keys are document URIs, and the values are Document bundles.
      */
     void getDocuments(in List<String> uris, in AndroidFuture<AppSearchBatchResult> callback);
 
@@ -69,8 +71,7 @@
      *
      * @param queryExpression String to search for
      * @param searchSpecBundle SearchSpec bundle
-     * @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link byte[]}&gt;&gt;
-     *     Will be completed with a serialized {@link SearchResultsProto}.
+     * @param callback {@link AndroidFuture}&lt;{@link AppSearchResult}&lt;{@link SearchResults}&gt;&gt;
      */
     void query(
         in String queryExpression,
diff --git a/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java b/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java
deleted file mode 100644
index 5ce2960..0000000
--- a/apex/appsearch/framework/java/android/app/appsearch/MatchInfo.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.appsearch;
-
-import android.annotation.NonNull;
-import android.util.Range;
-
-import com.google.android.icing.proto.SnippetMatchProto;
-
-/**
- * Snippet: It refers to a substring of text from the content of document that is returned as a
- * part of search result.
- * This class represents a match objects for any Snippets that might be present in
- * {@link SearchResults} from query. Using this class user can get the full text, exact matches and
- * Snippets of document content for a given match.
- *
- * <p>Class Example 1:
- * A document contains following text in property subject:
- * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
- *
- * <p>If the queryExpression is "foo".
- *
- * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
- * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another nonsense
- * word that’s used a lot is bar."
- * <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32]
- * <p>{@link MatchInfo#getExactMatch()} returns "foo"
- * <p>{@link MatchInfo#getSnippetPosition()} returns [29, 41]
- * <p>{@link MatchInfo#getSnippet()} returns "is foo. Another"
- * <p>
- * <p>Class Example 2:
- * A document contains a property name sender which contains 2 property names name and email, so
- * we will have 2 property paths: {@code sender.name} and {@code sender.email}.
- * <p> Let {@code sender.name = "Test Name Jr."} and {@code sender.email = "TestNameJr@gmail.com"}
- *
- * <p>If the queryExpression is "Test". We will have 2 matches.
- *
- * <p> Match-1
- * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
- * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
- * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4]
- * <p>{@link MatchInfo#getExactMatch()} returns "Test"
- * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
- * <p>{@link MatchInfo#getSnippet()} returns "Test Name Jr."
- * <p> Match-2
- * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
- * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
- * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20]
- * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
- * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
- * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
- * @hide
- */
-// TODO(sidchhabra): Capture real snippet after integration with icingLib.
-public final class MatchInfo {
-
-    private final String mPropertyPath;
-    private final SnippetMatchProto mSnippetMatch;
-    private final AppSearchDocument mDocument;
-    /**
-     * List of content with same property path in a document when there are multiple matches in
-     * repeated sections.
-     */
-    private final String[] mValues;
-
-    /** @hide */
-    public MatchInfo(@NonNull String propertyPath, @NonNull SnippetMatchProto snippetMatch,
-            @NonNull AppSearchDocument document) {
-        mPropertyPath = propertyPath;
-        mSnippetMatch = snippetMatch;
-        mDocument = document;
-        // In IcingLib snippeting is available for only 3 data types i.e String, double and long,
-        // so we need to check which of these three are requested.
-        // TODO (sidchhabra): getPropertyStringArray takes property name, handle for property path.
-        String[] values = mDocument.getPropertyStringArray(propertyPath);
-        if (values == null) {
-            values = doubleToString(mDocument.getPropertyDoubleArray(propertyPath));
-        }
-        if (values == null) {
-            values = longToString(mDocument.getPropertyLongArray(propertyPath));
-        }
-        if (values == null) {
-            throw new IllegalStateException("No content found for requested property path!");
-        }
-        mValues = values;
-    }
-
-    /**
-     * Gets the property path corresponding to the given entry.
-     * <p>Property Path: '.' - delimited sequence of property names indicating which property in
-     * the Document these snippets correspond to.
-     * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
-     * For class example 1 this returns "subject"
-     */
-    @NonNull
-    public String getPropertyPath() {
-        return mPropertyPath;
-    }
-
-    /**
-     * Gets the full text corresponding to the given entry.
-     * <p>For class example this returns "A commonly used fake word is foo. Another nonsense word
-     * that’s used a lot is bar."
-     */
-    @NonNull
-    public String getFullText() {
-        return mValues[mSnippetMatch.getValuesIndex()];
-    }
-
-    /**
-     * Gets the exact match range corresponding to the given entry.
-     * <p>For class example 1 this returns [29, 32]
-     */
-    @NonNull
-    public Range getExactMatchPosition() {
-        return new Range(mSnippetMatch.getExactMatchPosition(),
-                mSnippetMatch.getExactMatchPosition() + mSnippetMatch.getExactMatchBytes());
-    }
-
-    /**
-     * Gets the exact match corresponding to the given entry.
-     * <p>For class example 1 this returns "foo"
-     */
-    @NonNull
-    public CharSequence getExactMatch() {
-        return getSubstring(getExactMatchPosition());
-    }
-
-    /**
-     * Gets the snippet range corresponding to the given entry.
-     * <p>For class example 1 this returns [29, 41]
-     */
-    @NonNull
-    public Range getSnippetPosition() {
-        return new Range(mSnippetMatch.getWindowPosition(),
-                mSnippetMatch.getWindowPosition() + mSnippetMatch.getWindowBytes());
-    }
-
-    /**
-     * Gets the snippet corresponding to the given entry.
-     * <p>Snippet - Provides a subset of the content to display. The
-     * length of this content can be changed {@link SearchSpec.Builder#setMaxSnippetSize(int)}.
-     * Windowing is centered around the middle of the matched token with content on either side
-     * clipped to token boundaries.
-     * <p>For class example 1 this returns "foo. Another"
-     */
-    @NonNull
-    public CharSequence getSnippet() {
-        return getSubstring(getSnippetPosition());
-    }
-
-    private CharSequence getSubstring(Range range) {
-        return getFullText()
-                .substring((int) range.getLower(), (int) range.getUpper());
-    }
-
-    /** Utility method to convert double[] to String[] */
-    private String[] doubleToString(double[] values) {
-        //TODO(sidchhabra): Implement the method.
-        return null;
-    }
-
-    /** Utility method to convert long[] to String[] */
-    private String[] longToString(long[] values) {
-        //TODO(sidchhabra): Implement the method.
-        return null;
-    }
-}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResult.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResult.java
new file mode 100644
index 0000000..758280b
--- /dev/null
+++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResult.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.os.Bundle;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Objects;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents one of the results obtained from the query.
+ *
+ * <p>It contains the document which matched, information about which section(s) in the document
+ * matched, and snippet information containing textual summaries of the document's match(es).
+ * @hide
+ */
+public final class SearchResult {
+    /** @hide */
+    
+    public static final String DOCUMENT_FIELD = "document";
+
+    /** @hide */
+    
+    public static final String MATCHES_FIELD = "matches";
+
+    @NonNull
+    private final Bundle mBundle;
+
+    @NonNull
+    private final Bundle mDocumentBundle;
+
+    @Nullable
+    private GenericDocument mDocument;
+
+    @Nullable
+    private final List<Bundle> mMatchBundles;
+
+    /**
+     * Contains a list of Snippets that matched the request. Only populated when requested in
+     * both {@link SearchSpec.Builder#setSnippetCount(int)}
+     * and {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}.
+     *
+     * @see #getMatches()
+     */
+    @Nullable
+    private List<MatchInfo> mMatches;
+
+    /** @hide */
+    
+    public SearchResult(@NonNull Bundle bundle) {
+        mBundle = Preconditions.checkNotNull(bundle);
+        mDocumentBundle = Preconditions.checkNotNull(bundle.getBundle(DOCUMENT_FIELD));
+        mMatchBundles = bundle.getParcelableArrayList(MATCHES_FIELD);
+    }
+
+    /** @hide */
+    
+    @NonNull
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Contains the matching {@link GenericDocument}.
+     * @return Document object which matched the query.
+     */
+    @NonNull
+    public GenericDocument getDocument() {
+        if (mDocument == null) {
+            mDocument = new GenericDocument(mDocumentBundle);
+        }
+        return mDocument;
+    }
+
+    /**
+     * Contains a list of Snippets that matched the request. Only populated when requested in
+     * both {@link SearchSpec.Builder#setSnippetCount(int)}
+     * and {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}.
+     *
+     * @return  List of matches based on {@link SearchSpec}, if snippeting is disabled and this
+     * method is called it will return {@code null}. Users can also restrict snippet population
+     * using {@link SearchSpec.Builder#setSnippetCount} and
+     * {@link SearchSpec.Builder#setSnippetCountPerProperty(int)}, for all results after that
+     * value this method will return {@code null}.
+     */
+    @Nullable
+    public List<MatchInfo> getMatches() {
+        if (mMatchBundles != null && mMatches == null) {
+            mMatches = new ArrayList<>(mMatchBundles.size());
+            for (int i = 0; i < mMatchBundles.size(); i++) {
+                MatchInfo matchInfo = new MatchInfo(getDocument(), mMatchBundles.get(i));
+                mMatches.add(matchInfo);
+            }
+        }
+        return mMatches;
+    }
+
+    /**
+     * Snippet: It refers to a substring of text from the content of document that is returned as a
+     * part of search result.
+     * This class represents a match objects for any Snippets that might be present in
+     * {@link SearchResults} from query. Using this class user can get the full text, exact matches
+     * and Snippets of document content for a given match.
+     *
+     * <p>Class Example 1:
+     * A document contains following text in property subject:
+     * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
+     *
+     * <p>If the queryExpression is "foo".
+     *
+     * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
+     * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
+     * nonsense word that’s used a lot is bar."
+     * <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32]
+     * <p>{@link MatchInfo#getExactMatch()} returns "foo"
+     * <p>{@link MatchInfo#getSnippetPosition()} returns [26, 33]
+     * <p>{@link MatchInfo#getSnippet()} returns "is foo."
+     * <p>
+     * <p>Class Example 2:
+     * A document contains a property name sender which contains 2 property names name and email, so
+     * we will have 2 property paths: {@code sender.name} and {@code sender.email}.
+     * <p>Let {@code sender.name = "Test Name Jr."} and
+     * {@code sender.email = "TestNameJr@gmail.com"}
+     *
+     * <p>If the queryExpression is "Test". We will have 2 matches.
+     *
+     * <p> Match-1
+     * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
+     * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
+     * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4]
+     * <p>{@link MatchInfo#getExactMatch()} returns "Test"
+     * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
+     * <p>{@link MatchInfo#getSnippet()} returns "Test Name"
+     * <p> Match-2
+     * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
+     * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
+     * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20]
+     * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
+     * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
+     * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
+     */
+    public static final class MatchInfo {
+        /**
+         * The path of the matching snippet property.
+         * @hide
+         */
+        
+        public static final String PROPERTY_PATH_FIELD = "propertyPath";
+
+        /**
+         * The index of matching value in its property. A property may have multiple values. This
+         * index indicates which value is the match.
+         * @hide
+         */
+        
+        public static final String VALUES_INDEX_FIELD = "valuesIndex";
+
+        /** @hide */
+        
+        public static final String EXACT_MATCH_POSITION_LOWER_FIELD = "exactMatchPositionLower";
+
+        /** @hide */
+        
+        public static final String EXACT_MATCH_POSITION_UPPER_FIELD = "exactMatchPositionUpper";
+
+        /** @hide */
+        
+        public static final String WINDOW_POSITION_LOWER_FIELD = "windowPositionLower";
+
+        /** @hide */
+        
+        public static final String WINDOW_POSITION_UPPER_FIELD = "windowPositionUpper";
+
+        private final String mFullText;
+        private final String mPropertyPath;
+        private final Bundle mBundle;
+        private MatchRange mExactMatchRange;
+        private MatchRange mWindowRange;
+
+        MatchInfo(@NonNull GenericDocument document, @NonNull Bundle bundle) {
+            mBundle = Preconditions.checkNotNull(bundle);
+            Preconditions.checkNotNull(document);
+            mPropertyPath = Preconditions.checkNotNull(bundle.getString(PROPERTY_PATH_FIELD));
+            mFullText = getPropertyValues(
+                    document, mPropertyPath, mBundle.getInt(VALUES_INDEX_FIELD));
+        }
+
+        /**
+         * Gets the property path corresponding to the given entry.
+         * <p>Property Path: '.' - delimited sequence of property names indicating which property in
+         * the Document these snippets correspond to.
+         * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
+         * For class example 1 this returns "subject"
+         */
+        @NonNull
+        public String getPropertyPath() {
+            return mPropertyPath;
+        }
+
+        /**
+         * Gets the full text corresponding to the given entry.
+         * <p>For class example this returns "A commonly used fake word is foo. Another nonsense
+         * word that's used a lot is bar."
+         */
+        @NonNull
+        public String getFullText() {
+            return mFullText;
+        }
+
+        /**
+         * Gets the exact {@link MatchRange} corresponding to the given entry.
+         * <p>For class example 1 this returns [29, 32]
+         */
+        @NonNull
+        public MatchRange getExactMatchPosition() {
+            if (mExactMatchRange == null) {
+                mExactMatchRange = new MatchRange(
+                        mBundle.getInt(EXACT_MATCH_POSITION_LOWER_FIELD),
+                        mBundle.getInt(EXACT_MATCH_POSITION_UPPER_FIELD));
+            }
+            return mExactMatchRange;
+        }
+
+        /**
+         * Gets the  {@link MatchRange} corresponding to the given entry.
+         * <p>For class example 1 this returns "foo"
+         */
+        @NonNull
+        public CharSequence getExactMatch() {
+            return getSubstring(getExactMatchPosition());
+        }
+
+        /**
+         * Gets the snippet {@link MatchRange} corresponding to the given entry.
+         * <p>Only populated when set maxSnippetSize > 0 in
+         * {@link SearchSpec.Builder#setMaxSnippetSize}.
+         * <p>For class example 1 this returns [29, 41].
+         */
+        @NonNull
+        public MatchRange getSnippetPosition() {
+            if (mWindowRange == null) {
+                mWindowRange = new MatchRange(
+                        mBundle.getInt(WINDOW_POSITION_LOWER_FIELD),
+                        mBundle.getInt(WINDOW_POSITION_UPPER_FIELD));
+            }
+            return mWindowRange;
+        }
+
+        /**
+         * Gets the snippet corresponding to the given entry.
+         * <p>Snippet - Provides a subset of the content to display. Only populated when requested
+         * maxSnippetSize > 0. The size of this content can be changed by
+         * {@link SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of
+         * the matched token with content on either side clipped to token boundaries.
+         * <p>For class example 1 this returns "foo. Another"
+         */
+        @NonNull
+        public CharSequence getSnippet() {
+            return getSubstring(getSnippetPosition());
+        }
+
+        private CharSequence getSubstring(MatchRange range) {
+            return getFullText().substring(range.getStart(), range.getEnd());
+        }
+
+        /** Extracts the matching string from the document. */
+        private static String getPropertyValues(
+                GenericDocument document, String propertyName, int valueIndex) {
+            // In IcingLib snippeting is available for only 3 data types i.e String, double and
+            // long, so we need to check which of these three are requested.
+            // TODO (tytytyww): getPropertyStringArray takes property name, handle for property
+            //  path.
+            // TODO (tytytyww): support double[] and long[].
+            String[] values = document.getPropertyStringArray(propertyName);
+            if (values == null) {
+                throw new IllegalStateException("No content found for requested property path!");
+            }
+            return values[valueIndex];
+        }
+    }
+
+    /**
+     * Class providing the position range of matching information.
+     *
+     * <p> All ranges are finite, and the left side of the range is always {@code <=} the right
+     * side of the range.
+     *
+     * <p> Example: MatchRange(0, 100) represent a hundred ints from 0 to 99."
+     *
+     */
+    public static final class MatchRange {
+        private final int mEnd;
+        private final int mStart;
+
+        /**
+         * Creates a new immutable range.
+         * <p> The endpoints are {@code [start, end)}; that is the range is bounded. {@code start}
+         * must be lesser or equal to {@code end}.
+         *
+         * @param start The start point (inclusive)
+         * @param end The end point (exclusive)
+         * @hide
+         */
+        
+        public MatchRange(int start, int end) {
+            if (start > end) {
+                throw new IllegalArgumentException("Start point must be less than or equal to "
+                        + "end point");
+            }
+            mStart = start;
+            mEnd = end;
+        }
+
+        /** Gets the start point (inclusive). */
+        public int getStart() {
+            return mStart;
+        }
+
+        /** Gets the end point (exclusive). */
+        public int getEnd() {
+            return mEnd;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof MatchRange)) {
+                return false;
+            }
+            MatchRange otherMatchRange = (MatchRange) other;
+            return this.getStart() == otherMatchRange.getStart()
+                    && this.getEnd() == otherMatchRange.getEnd();
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "MatchRange { start: " + mStart + " , end: " + mEnd + "}";
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mStart, mEnd);
+        }
+    }
+}
diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java
index 7287fe6..9f37625 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java
@@ -17,112 +17,62 @@
 package android.app.appsearch;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.SnippetMatchProto;
-import com.google.android.icing.proto.SnippetProto;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
-import java.util.NoSuchElementException;
 
 /**
- * SearchResults are a list of results that are returned from a query. Each result from this
- * list contains a document and may contain other fields like snippets based on request.
- * This iterator class is not thread safe.
+ * Structure for transmitting a page of search results across binder.
  * @hide
  */
-public final class SearchResults implements Iterator<SearchResults.Result> {
+public final class SearchResults implements Parcelable {
+    final List<SearchResult> mResults;
+    final long mNextPageToken;
 
-    private final SearchResultProto mSearchResultProto;
-    private int mNextIdx;
+    public SearchResults(@NonNull List<SearchResult> results, long nextPageToken) {
+        mResults = results;
+        mNextPageToken = nextPageToken;
+    }
 
-    /** @hide */
-    public SearchResults(SearchResultProto searchResultProto) {
-        mSearchResultProto = searchResultProto;
+    private SearchResults(@NonNull Parcel in) {
+        List<Bundle> resultBundles = in.readArrayList(/*loader=*/ null);
+        mResults = new ArrayList<>(resultBundles.size());
+        for (int i = 0; i < resultBundles.size(); i++) {
+            SearchResult searchResult = new SearchResult(resultBundles.get(i));
+            mResults.add(searchResult);
+        }
+        mNextPageToken = in.readLong();
     }
 
     @Override
-    public boolean hasNext() {
-        return mNextIdx < mSearchResultProto.getResultsCount();
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        List<Bundle> resultBundles = new ArrayList<>(mResults.size());
+        for (int i = 0; i < mResults.size(); i++) {
+            resultBundles.add(mResults.get(i).getBundle());
+        }
+        dest.writeList(resultBundles);
+        dest.writeLong(mNextPageToken);
     }
 
-    @NonNull
     @Override
-    public Result next() {
-        if (!hasNext()) {
-            throw new NoSuchElementException();
-        }
-        Result result = new Result(mSearchResultProto.getResults(mNextIdx));
-        mNextIdx++;
-        return result;
+    public int describeContents() {
+        return 0;
     }
 
-
-
-    /**
-     * This class represents the result obtained from the query. It will contain the document which
-     * which matched the specified query string and specifications.
-     * @hide
-     */
-    public static final class Result {
-        private final SearchResultProto.ResultProto mResultProto;
-
-        @Nullable
-        private AppSearchDocument mDocument;
-
-        private Result(SearchResultProto.ResultProto resultProto) {
-            mResultProto = resultProto;
-        }
-
-        /**
-         * Contains the matching {@link AppSearchDocument}.
-         * @return Document object which matched the query.
-         * @hide
-         */
+    public static final Creator<SearchResults> CREATOR = new Creator<SearchResults>() {
         @NonNull
-        public AppSearchDocument getDocument() {
-            if (mDocument == null) {
-                mDocument = new AppSearchDocument(mResultProto.getDocument());
-            }
-            return mDocument;
+        @Override
+        public SearchResults createFromParcel(@NonNull Parcel in) {
+            return new SearchResults(in);
         }
 
-        /**
-         * Contains a list of Snippets that matched the request. Only populated when requested in
-         * {@link SearchSpec.Builder#setMaxSnippetSize(int)}.
-         * @return  List of matches based on {@link SearchSpec}, if snippeting is disabled and this
-         * method is called it will return {@code null}. Users can also restrict snippet population
-         * using {@link SearchSpec.Builder#setNumToSnippet} and
-         * {@link SearchSpec.Builder#setNumMatchesPerProperty}, for all results after that value
-         * this method will return {@code null}.
-         * @hide
-         */
-        // TODO(sidchhabra): Replace Document with proper constructor.
-        @Nullable
-        public List<MatchInfo> getMatchInfo() {
-            if (!mResultProto.hasSnippet()) {
-                return null;
-            }
-            AppSearchDocument document = getDocument();
-            List<MatchInfo> matchList = new ArrayList<>();
-            for (Iterator entryProtoIterator = mResultProto.getSnippet()
-                    .getEntriesList().iterator(); entryProtoIterator.hasNext(); ) {
-                SnippetProto.EntryProto entry = (SnippetProto.EntryProto) entryProtoIterator.next();
-                for (Iterator snippetMatchProtoIterator = entry.getSnippetMatchesList().iterator();
-                        snippetMatchProtoIterator.hasNext(); ) {
-                    matchList.add(new MatchInfo(entry.getPropertyName(),
-                            (SnippetMatchProto) snippetMatchProtoIterator.next(), document));
-                }
-            }
-            return matchList;
+        @NonNull
+        @Override
+        public SearchResults[] newArray(int size) {
+            return new SearchResults[size];
         }
-    }
-
-    @Override
-    public String toString() {
-        return mSearchResultProto.toString();
-    }
+    };
 }
diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java
index 817c3ef..c871905 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/SearchSpec.java
@@ -249,8 +249,10 @@
         /**
          * Only the first {@code snippetCount} documents based on the ranking strategy
          * will have snippet information provided.
-         * <p>If set to 0 (default), snippeting is disabled and
-         * {@link SearchResults.Result#getMatches} will return {@code null} for that result.
+         *
+         * <p>If set to 0 (default), snippeting is disabled and {@link SearchResult#getMatches} will
+         * return {@code null} for that result.
+         *
          * <p>The value should be set in range[0, 10k].
          */
         @NonNull
@@ -264,8 +266,10 @@
         /**
          * Only the first {@code matchesCountPerProperty} matches for a every property of
          * {@link GenericDocument} will contain snippet information.
-         * <p>If set to 0, snippeting is disabled and {@link SearchResults.Result#getMatches}
-         * will return {@code null} for that result.
+         *
+         * <p>If set to 0, snippeting is disabled and {@link SearchResult#getMatches} will return
+         * {@code null} for that result.
+         *
          * <p>The value should be set in range[0, 10k].
          */
         @NonNull
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 4bc0c39..06612ac 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -17,10 +17,12 @@
 
 import android.annotation.NonNull;
 import android.app.appsearch.AppSearchBatchResult;
-import android.app.appsearch.AppSearchDocument;
 import android.app.appsearch.AppSearchResult;
 import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
 import android.app.appsearch.IAppSearchManager;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResults;
 import android.app.appsearch.SearchSpec;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.content.Context;
@@ -32,7 +34,9 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.SystemService;
 import com.android.server.appsearch.external.localbackend.AppSearchImpl;
+import com.android.server.appsearch.external.localbackend.converter.GenericDocumentToProtoConverter;
 import com.android.server.appsearch.external.localbackend.converter.SchemaToProtoConverter;
+import com.android.server.appsearch.external.localbackend.converter.SearchResultToProtoConverter;
 import com.android.server.appsearch.external.localbackend.converter.SearchSpecToProtoConverter;
 
 import com.google.android.icing.proto.DocumentProto;
@@ -80,7 +84,7 @@
                 AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
                 String databaseName = makeDatabaseName(callingUid);
                 impl.setSchema(databaseName, schemaProtoBuilder.build(), forceOverride);
-                callback.complete(AppSearchResult.newSuccessfulResult(/*value=*/ null));
+                callback.complete(AppSearchResult.newSuccessfulResult(/*result=*/ null));
             } catch (Throwable t) {
                 callback.complete(throwableToFailedResult(t));
             } finally {
@@ -90,9 +94,9 @@
 
         @Override
         public void putDocuments(
-                @NonNull List documentsBytes,
+                @NonNull List<Bundle> documentBundles,
                 @NonNull AndroidFuture<AppSearchBatchResult> callback) {
-            Preconditions.checkNotNull(documentsBytes);
+            Preconditions.checkNotNull(documentBundles);
             Preconditions.checkNotNull(callback);
             int callingUid = Binder.getCallingUidOrThrow();
             int callingUserId = UserHandle.getUserId(callingUid);
@@ -102,12 +106,12 @@
                 String databaseName = makeDatabaseName(callingUid);
                 AppSearchBatchResult.Builder<String, Void> resultBuilder =
                         new AppSearchBatchResult.Builder<>();
-                for (int i = 0; i < documentsBytes.size(); i++) {
-                    byte[] documentBytes = (byte[]) documentsBytes.get(i);
-                    DocumentProto document = DocumentProto.parseFrom(documentBytes);
+                for (int i = 0; i < documentBundles.size(); i++) {
+                    GenericDocument document = new GenericDocument(documentBundles.get(i));
+                    DocumentProto documentProto = GenericDocumentToProtoConverter.convert(document);
                     try {
-                        impl.putDocument(databaseName, document);
-                        resultBuilder.setSuccess(document.getUri(), /*value=*/ null);
+                        impl.putDocument(databaseName, documentProto);
+                        resultBuilder.setSuccess(document.getUri(), /*result=*/ null);
                     } catch (Throwable t) {
                         resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
                     }
@@ -131,18 +135,20 @@
             try {
                 AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
                 String databaseName = makeDatabaseName(callingUid);
-                AppSearchBatchResult.Builder<String, byte[]> resultBuilder =
+                AppSearchBatchResult.Builder<String, Bundle> resultBuilder =
                         new AppSearchBatchResult.Builder<>();
                 for (int i = 0; i < uris.size(); i++) {
                     String uri = uris.get(i);
                     try {
-                        DocumentProto document = impl.getDocument(
-                                databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri);
-                        if (document == null) {
+                        DocumentProto documentProto = impl.getDocument(
+                                databaseName, GenericDocument.DEFAULT_NAMESPACE, uri);
+                        if (documentProto == null) {
                             resultBuilder.setFailure(
                                     uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null);
                         } else {
-                            resultBuilder.setSuccess(uri, document.toByteArray());
+                            GenericDocument genericDocument =
+                                    GenericDocumentToProtoConverter.convert(documentProto);
+                            resultBuilder.setSuccess(uri, genericDocument.getBundle());
                         }
                     } catch (Throwable t) {
                         resultBuilder.setResult(uri, throwableToFailedResult(t));
@@ -182,8 +188,11 @@
                         searchSpecProto,
                         SearchSpecToProtoConverter.toResultSpecProto(searchSpec),
                         SearchSpecToProtoConverter.toScoringSpecProto(searchSpec));
-                callback.complete(
-                        AppSearchResult.newSuccessfulResult(searchResultProto.toByteArray()));
+                List<SearchResult> searchResultList =
+                        SearchResultToProtoConverter.convert(searchResultProto);
+                SearchResults searchResults =
+                        new SearchResults(searchResultList, searchResultProto.getNextPageToken());
+                callback.complete(AppSearchResult.newSuccessfulResult(searchResults));
             } catch (Throwable t) {
                 callback.complete(throwableToFailedResult(t));
             } finally {
@@ -206,8 +215,8 @@
                 for (int i = 0; i < uris.size(); i++) {
                     String uri = uris.get(i);
                     try {
-                        impl.remove(databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri);
-                        resultBuilder.setSuccess(uri, /*value= */null);
+                        impl.remove(databaseName, GenericDocument.DEFAULT_NAMESPACE, uri);
+                        resultBuilder.setSuccess(uri, /*result= */null);
                     } catch (Throwable t) {
                         resultBuilder.setResult(uri, throwableToFailedResult(t));
                     }
@@ -237,7 +246,7 @@
                     String schemaType = schemaTypes.get(i);
                     try {
                         impl.removeByType(databaseName, schemaType);
-                        resultBuilder.setSuccess(schemaType, /*value=*/ null);
+                        resultBuilder.setSuccess(schemaType, /*result=*/ null);
                     } catch (Throwable t) {
                         resultBuilder.setResult(schemaType, throwableToFailedResult(t));
                     }
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverter.java
new file mode 100644
index 0000000..fdeb90d
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverter.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appsearch.external.localbackend.converter;
+
+import android.os.Bundle;
+
+import android.annotation.NonNull;
+
+import android.app.appsearch.GenericDocument;
+import com.android.internal.util.Preconditions;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyProto;
+import com.google.protobuf.ByteString;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Translates a {@link GenericDocument} into a {@link DocumentProto}.
+ * @hide
+ */
+
+public final class GenericDocumentToProtoConverter {
+    private GenericDocumentToProtoConverter() {}
+
+    /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */
+    @NonNull
+    @SuppressWarnings("unchecked")
+    public static DocumentProto convert(@NonNull GenericDocument document) {
+        Preconditions.checkNotNull(document);
+        Bundle properties = document.getBundle().getBundle(GenericDocument.PROPERTIES_FIELD);
+        DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
+        mProtoBuilder.setUri(document.getUri())
+                .setSchema(document.getSchemaType())
+                .setNamespace(document.getNamespace())
+                .setScore(document.getScore())
+                .setTtlMs(document.getTtlMillis())
+                .setCreationTimestampMs(document.getCreationTimestampMillis());
+        ArrayList<String> keys = new ArrayList<>(properties.keySet());
+        Collections.sort(keys);
+        for (int i = 0; i < keys.size(); i++) {
+            String name = keys.get(i);
+            Object values = properties.get(name);
+            PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(name);
+            if (values instanceof boolean[]) {
+                for (boolean value : (boolean[]) values) {
+                    propertyProto.addBooleanValues(value);
+                }
+            } else if (values instanceof long[]) {
+                for (long value : (long[]) values) {
+                    propertyProto.addInt64Values(value);
+                }
+            } else if (values instanceof double[]) {
+                for (double value : (double[]) values) {
+                    propertyProto.addDoubleValues(value);
+                }
+            } else if (values instanceof String[]) {
+                for (String value : (String[]) values) {
+                    propertyProto.addStringValues(value);
+                }
+            } else if (values instanceof ArrayList) {
+                for (Bundle bundle : (ArrayList<Bundle>) values) {
+                    byte[] value = bundle.getByteArray(GenericDocument.BYTE_ARRAY_FIELD);
+                    propertyProto.addBytesValues(ByteString.copyFrom(value));
+                }
+            } else if (values instanceof Bundle[]) {
+                for (Bundle bundle : (Bundle[]) values) {
+                    GenericDocument value = new GenericDocument(bundle);
+                    propertyProto.addDocumentValues(convert(value));
+                }
+            } else {
+                throw new IllegalStateException(
+                        "Property \"" + name + "\" has unsupported value type \""
+                                + values.getClass().getSimpleName() + "\"");
+            }
+            mProtoBuilder.addProperties(propertyProto);
+        }
+        return mProtoBuilder.build();
+    }
+
+    /** Converts a {@link DocumentProto} into a {@link GenericDocument}. */
+    @NonNull
+    public static GenericDocument convert(@NonNull DocumentProto proto) {
+        Preconditions.checkNotNull(proto);
+        GenericDocument.Builder<?> documentBuilder =
+                new GenericDocument.Builder<>(proto.getUri(), proto.getSchema())
+                        .setNamespace(proto.getNamespace())
+                        .setScore(proto.getScore())
+                        .setTtlMillis(proto.getTtlMs())
+                        .setCreationTimestampMillis(proto.getCreationTimestampMs());
+
+        for (int i = 0; i < proto.getPropertiesCount(); i++) {
+            PropertyProto property = proto.getProperties(i);
+            String name = property.getName();
+            if (property.getBooleanValuesCount() > 0) {
+                boolean[] values = new boolean[property.getBooleanValuesCount()];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = property.getBooleanValues(j);
+                }
+                documentBuilder.setProperty(name, values);
+            } else if (property.getInt64ValuesCount() > 0) {
+                long[] values = new long[property.getInt64ValuesCount()];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = property.getInt64Values(j);
+                }
+                documentBuilder.setProperty(name, values);
+            } else if (property.getDoubleValuesCount() > 0) {
+                double[] values = new double[property.getDoubleValuesCount()];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = property.getDoubleValues(j);
+                }
+                documentBuilder.setProperty(name, values);
+            } else if (property.getStringValuesCount() > 0) {
+                String[] values = new String[property.getStringValuesCount()];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = property.getStringValues(j);
+                }
+                documentBuilder.setProperty(name, values);
+            } else if (property.getBytesValuesCount() > 0) {
+                byte[][] values = new byte[property.getBytesValuesCount()][];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = property.getBytesValues(j).toByteArray();
+                }
+                documentBuilder.setProperty(name, values);
+            } else if (property.getDocumentValuesCount() > 0) {
+                GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = convert(property.getDocumentValues(j));
+                }
+                documentBuilder.setProperty(name, values);
+            } else {
+                throw new IllegalStateException("Unknown type of value: " + name);
+            }
+        }
+        return documentBuilder.build();
+    }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchResultToProtoConverter.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchResultToProtoConverter.java
new file mode 100644
index 0000000..524c80d
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/converter/SearchResultToProtoConverter.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appsearch.external.localbackend.converter;
+
+import android.os.Bundle;
+
+import android.annotation.NonNull;
+
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResults;
+
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.proto.SnippetMatchProto;
+import com.google.android.icing.proto.SnippetProto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Translates a {@link SearchResultProto} into {@link SearchResults}.
+ * @hide
+ */
+
+public class SearchResultToProtoConverter {
+    private SearchResultToProtoConverter() {}
+
+    /** Translates a {@link SearchResultProto} into a list of {@link SearchResult}. */
+    @NonNull
+    public static List<SearchResult> convert(@NonNull SearchResultProto searchResultProto) {
+        List<SearchResult> results = new ArrayList<>(searchResultProto.getResultsCount());
+        for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
+            results.add(convertSearchResult(searchResultProto.getResults(i)));
+        }
+        return results;
+    }
+
+    /** Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. */
+    @NonNull
+    static SearchResult convertSearchResult(@NonNull SearchResultProto.ResultProto proto) {
+        Bundle bundle = new Bundle();
+        GenericDocument document = GenericDocumentToProtoConverter.convert(proto.getDocument());
+        bundle.putBundle(SearchResult.DOCUMENT_FIELD, document.getBundle());
+
+        ArrayList<Bundle> matchList = null;
+        if (proto.hasSnippet()) {
+            matchList = new ArrayList<>();
+            for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) {
+                SnippetProto.EntryProto entry = proto.getSnippet().getEntries(i);
+                for (int j = 0; j < entry.getSnippetMatchesCount(); j++) {
+                    Bundle matchInfoBundle = convertToMatchInfoBundle(
+                            entry.getSnippetMatches(j), entry.getPropertyName());
+                    matchList.add(matchInfoBundle);
+                }
+            }
+        }
+        bundle.putParcelableArrayList(SearchResult.MATCHES_FIELD, matchList);
+
+        return new SearchResult(bundle);
+    }
+
+    private static Bundle convertToMatchInfoBundle(
+            SnippetMatchProto snippetMatchProto, String propertyPath) {
+        Bundle bundle = new Bundle();
+        bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, propertyPath);
+        bundle.putInt(
+                SearchResult.MatchInfo.VALUES_INDEX_FIELD, snippetMatchProto.getValuesIndex());
+        bundle.putInt(
+                SearchResult.MatchInfo.EXACT_MATCH_POSITION_LOWER_FIELD,
+                snippetMatchProto.getExactMatchPosition());
+        bundle.putInt(
+                SearchResult.MatchInfo.EXACT_MATCH_POSITION_UPPER_FIELD,
+                snippetMatchProto.getExactMatchPosition() + snippetMatchProto.getExactMatchBytes());
+        bundle.putInt(
+                SearchResult.MatchInfo.WINDOW_POSITION_LOWER_FIELD,
+                snippetMatchProto.getWindowPosition());
+        bundle.putInt(
+                SearchResult.MatchInfo.WINDOW_POSITION_UPPER_FIELD,
+                snippetMatchProto.getWindowPosition() + snippetMatchProto.getWindowBytes());
+        return bundle;
+    }
+}
diff --git a/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java b/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java
deleted file mode 100644
index ac0f44b..0000000
--- a/core/tests/coretests/src/android/app/appsearch/SearchResultsTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.appsearch;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.app.appsearch.proto.DocumentProto;
-import android.app.appsearch.proto.SearchResultProto;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-
-@SmallTest
-public class SearchResultsTest {
-
-    @Test
-    public void testSearchResultsEqual() {
-        final String uri = "testUri";
-        final String schemaType = "testSchema";
-        SearchResultProto.ResultProto result1 = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(DocumentProto.newBuilder()
-                        .setUri(uri)
-                        .setSchema(schemaType)
-                        .build())
-                .build();
-        SearchResultProto searchResults1 = SearchResultProto.newBuilder()
-                .addResults(result1)
-                .build();
-        SearchResults res1 = new SearchResults(searchResults1);
-        SearchResultProto.ResultProto result2 = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(DocumentProto.newBuilder()
-                        .setUri(uri)
-                        .setSchema(schemaType)
-                        .build())
-                .build();
-        SearchResultProto searchResults2 = SearchResultProto.newBuilder()
-                .addResults(result2)
-                .build();
-        SearchResults res2 = new SearchResults(searchResults2);
-        assertThat(res1.toString()).isEqualTo(res2.toString());
-    }
-
-    @Test
-    public void buildSearchSpecWithoutTermMatchType() {
-        assertThrows(RuntimeException.class, () -> new SearchSpec.Builder()
-                .setSchemaTypes("testSchemaType")
-                .build());
-    }
-}
diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchEmailTest.java
similarity index 94%
rename from core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java
rename to core/tests/coretests/src/android/app/appsearch/external/app/AppSearchEmailTest.java
index 6aa16cc..ac2d4b5 100644
--- a/core/tests/coretests/src/android/app/appsearch/AppSearchEmailTest.java
+++ b/core/tests/coretests/src/android/app/appsearch/external/app/AppSearchEmailTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,11 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.filters.SmallTest;
-
 import org.junit.Test;
 
-@SmallTest
 public class AppSearchEmailTest {
 
     @Test
diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java
similarity index 69%
rename from core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
rename to core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java
index 54a281f2..1f2c12b 100644
--- a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
+++ b/core/tests/coretests/src/android/app/appsearch/external/app/GenericDocumentTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,36 +18,25 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.testng.Assert.assertThrows;
-
-import android.app.appsearch.proto.DocumentProto;
-import android.app.appsearch.proto.PropertyProto;
-import android.app.appsearch.protobuf.ByteString;
-
-import androidx.test.filters.SmallTest;
-
+import static org.testng.Assert.expectThrows;
 
 import org.junit.Test;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-
-@SmallTest
-public class AppSearchDocumentTest {
+public class GenericDocumentTest {
     private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
-    private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};
-    private static final AppSearchDocument sDocumentProperties1 = new AppSearchDocument
+    private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
+    private static final GenericDocument sDocumentProperties1 = new GenericDocument
             .Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+            .setCreationTimestampMillis(12345L)
             .build();
-    private static final AppSearchDocument sDocumentProperties2 = new AppSearchDocument
+    private static final GenericDocument sDocumentProperties2 = new GenericDocument
             .Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+            .setCreationTimestampMillis(6789L)
             .build();
 
     @Test
     public void testDocumentEquals_Identical() {
-        AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setTtlMillis(1L)
                 .setProperty("longKey1", 1L, 2L, 3L)
@@ -57,7 +46,7 @@
                 .setProperty("byteKey1", sByteArray1, sByteArray2)
                 .setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2)
                 .build();
-        AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setTtlMillis(1L)
                 .setProperty("longKey1", 1L, 2L, 3L)
@@ -73,7 +62,7 @@
 
     @Test
     public void testDocumentEquals_DifferentOrder() {
-        AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setProperty("longKey1", 1L, 2L, 3L)
                 .setProperty("byteKey1", sByteArray1, sByteArray2)
@@ -84,7 +73,7 @@
                 .build();
 
         // Create second document with same parameter but different order.
-        AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setProperty("booleanKey1", true, false, true)
                 .setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2)
@@ -99,13 +88,13 @@
 
     @Test
     public void testDocumentEquals_Failure() {
-        AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setProperty("longKey1", 1L, 2L, 3L)
                 .build();
 
         // Create second document with same order but different value.
-        AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setProperty("longKey1", 1L, 2L, 4L) // Different
                 .build();
@@ -115,13 +104,13 @@
 
     @Test
     public void testDocumentEquals_Failure_RepeatedFieldOrder() {
-        AppSearchDocument document1 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setProperty("booleanKey1", true, false, true)
                 .build();
 
         // Create second document with same order but different value.
-        AppSearchDocument document2 = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setProperty("booleanKey1", true, true, false) // Different
                 .build();
@@ -131,11 +120,10 @@
 
     @Test
     public void testDocumentGetSingleValue() {
-        AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setScore(1)
                 .setTtlMillis(1L)
-                .setScore(1)
                 .setProperty("longKey1", 1L)
                 .setProperty("doubleKey1", 1.0)
                 .setProperty("booleanKey1", true)
@@ -159,7 +147,7 @@
 
     @Test
     public void testDocumentGetArrayValues() {
-        AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setProperty("longKey1", 1L, 2L, 3L)
                 .setProperty("doubleKey1", 1.0, 2.0, 3.0)
@@ -185,8 +173,51 @@
     }
 
     @Test
+    public void testDocument_ToString() throws Exception {
+        GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setProperty("longKey1", 1L, 2L, 3L)
+                .setProperty("doubleKey1", 1.0, 2.0, 3.0)
+                .setProperty("booleanKey1", true, false, true)
+                .setProperty("stringKey1", "String1", "String2", "String3")
+                .setProperty("byteKey1", sByteArray1, sByteArray2)
+                .setProperty("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .build();
+        String exceptedString = "{ key: 'creationTimestampMillis' value: 5 } "
+                + "{ key: 'namespace' value:  } "
+                + "{ key: 'properties' value: "
+                +       "{ key: 'booleanKey1' value: [ 'true' 'false' 'true' ] } "
+                +       "{ key: 'byteKey1' value: "
+                +             "{ key: 'byteArray' value: [ '1' '2' '3' ] } "
+                +             "{ key: 'byteArray' value: [ '4' '5' '6' '7' ] }  } "
+                +       "{ key: 'documentKey1' value: [ '"
+                +             "{ key: 'creationTimestampMillis' value: 12345 } "
+                +             "{ key: 'namespace' value:  } "
+                +             "{ key: 'properties' value:  } "
+                +             "{ key: 'schemaType' value: sDocumentPropertiesSchemaType1 } "
+                +             "{ key: 'score' value: 0 } "
+                +             "{ key: 'ttlMillis' value: 0 } "
+                +             "{ key: 'uri' value: sDocumentProperties1 } ' '"
+                +             "{ key: 'creationTimestampMillis' value: 6789 } "
+                +             "{ key: 'namespace' value:  } "
+                +             "{ key: 'properties' value:  } "
+                +             "{ key: 'schemaType' value: sDocumentPropertiesSchemaType2 } "
+                +             "{ key: 'score' value: 0 } "
+                +             "{ key: 'ttlMillis' value: 0 } "
+                +             "{ key: 'uri' value: sDocumentProperties2 } ' ] } "
+                +       "{ key: 'doubleKey1' value: [ '1.0' '2.0' '3.0' ] } "
+                +       "{ key: 'longKey1' value: [ '1' '2' '3' ] } "
+                +       "{ key: 'stringKey1' value: [ 'String1' 'String2' 'String3' ] }  } "
+                + "{ key: 'schemaType' value: schemaType1 } "
+                + "{ key: 'score' value: 0 } "
+                + "{ key: 'ttlMillis' value: 0 } "
+                + "{ key: 'uri' value: uri1 } ";
+        assertThat(document.toString()).isEqualTo(exceptedString);
+    }
+
+    @Test
     public void testDocumentGetValues_DifferentTypes() {
-        AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
+        GenericDocument document = new GenericDocument.Builder("uri1", "schemaType1")
                 .setScore(1)
                 .setProperty("longKey1", 1L)
                 .setProperty("booleanKey1", true, false, true)
@@ -213,53 +244,8 @@
 
     @Test
     public void testDocumentInvalid() {
-        AppSearchDocument.Builder builder = new AppSearchDocument.Builder("uri1", "schemaType1");
-        assertThrows(
+        GenericDocument.Builder builder = new GenericDocument.Builder("uri1", "schemaType1");
+        expectThrows(
                 IllegalArgumentException.class, () -> builder.setProperty("test", new boolean[]{}));
     }
-
-    @Test
-    public void testDocumentProtoPopulation() {
-        AppSearchDocument document = new AppSearchDocument.Builder("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setScore(1)
-                .setTtlMillis(1L)
-                .setProperty("longKey1", 1L)
-                .setProperty("doubleKey1", 1.0)
-                .setProperty("booleanKey1", true)
-                .setProperty("stringKey1", "test-value1")
-                .setProperty("byteKey1", sByteArray1)
-                .setProperty("documentKey1", sDocumentProperties1)
-                .build();
-
-        // Create the Document proto. Need to sort the property order by key.
-        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
-                .setUri("uri1")
-                .setSchema("schemaType1")
-                .setCreationTimestampMs(5L)
-                .setScore(1)
-                .setTtlMs(1L)
-                .setNamespace("");
-        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
-        propertyProtoMap.put("longKey1",
-                PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
-        propertyProtoMap.put("doubleKey1",
-                PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0));
-        propertyProtoMap.put("booleanKey1",
-                PropertyProto.newBuilder().setName("booleanKey1").addBooleanValues(true));
-        propertyProtoMap.put("stringKey1",
-                PropertyProto.newBuilder().setName("stringKey1").addStringValues("test-value1"));
-        propertyProtoMap.put("byteKey1",
-                PropertyProto.newBuilder().setName("byteKey1").addBytesValues(
-                        ByteString.copyFrom(sByteArray1)));
-        propertyProtoMap.put("documentKey1",
-                PropertyProto.newBuilder().setName("documentKey1")
-                        .addDocumentValues(sDocumentProperties1.getProto()));
-        List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
-        Collections.sort(sortedKey);
-        for (int i = 0; i < sortedKey.size(); i++) {
-            documentProtoBuilder.addProperties(propertyProtoMap.get(sortedKey.get(i)));
-        }
-        assertThat(document.getProto()).isEqualTo(documentProtoBuilder.build());
-    }
 }
diff --git a/core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java
new file mode 100644
index 0000000..acbf11a
--- /dev/null
+++ b/core/tests/coretests/src/android/app/appsearch/external/app/SearchResultsTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import static org.testng.Assert.expectThrows;
+
+import org.junit.Test;
+
+public class SearchResultsTest {
+    @Test
+    public void buildSearchSpecWithoutTermMatchType() {
+        expectThrows(RuntimeException.class, () -> new SearchSpec.Builder()
+                .setSchemaTypes("testSchemaType")
+                .build());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java
similarity index 80%
rename from core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java
rename to core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java
index b29483c..2c7c35f 100644
--- a/core/tests/coretests/src/android/app/appsearch/impl/CustomerDocumentTest.java
+++ b/core/tests/coretests/src/android/app/appsearch/external/app/customer/CustomerDocumentTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,33 +14,30 @@
  * limitations under the License.
  */
 
-package android.app.appsearch.impl;
-
-import static com.google.common.truth.Truth.assertThat;
+package android.app.appsearch.customer;
 
 import android.annotation.NonNull;
-import android.app.appsearch.AppSearchDocument;
+import android.app.appsearch.GenericDocument;
 
-import androidx.test.filters.SmallTest;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
 /**
- * Tests that {@link AppSearchDocument} and {@link AppSearchDocument.Builder} are extendable by
+ * Tests that {@link GenericDocument} and {@link GenericDocument.Builder} are extendable by
  * developers.
  *
- * <p>This class is intentionally in a different package than {@link AppSearchDocument} to make sure
+ * <p>This class is intentionally in a different package than {@link GenericDocument} to make sure
  * there are no package-private methods required for external developers to add custom types.
  */
-@SmallTest
 public class CustomerDocumentTest {
 
     private static byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
     private static byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};
-    private static AppSearchDocument sDocumentProperties1 = new AppSearchDocument
+    private static GenericDocument sDocumentProperties1 = new GenericDocument
             .Builder("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
             .build();
-    private static AppSearchDocument sDocumentProperties2 = new AppSearchDocument
+    private static GenericDocument sDocumentProperties2 = new GenericDocument
             .Builder("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
             .build();
 
@@ -77,19 +74,21 @@
 
     /**
      * An example document type for test purposes, defined outside of
-     * {@link android.app.appsearch.AppSearch} (the way an external developer would define it).
+     * {@link GenericDocument} (the way an external developer would define
+     * it).
      */
-    private static class CustomerDocument extends AppSearchDocument {
-        private CustomerDocument(AppSearchDocument document) {
+    private static class CustomerDocument extends GenericDocument {
+        private CustomerDocument(GenericDocument document) {
             super(document);
         }
 
-        public static class Builder extends AppSearchDocument.Builder<CustomerDocument.Builder> {
+        public static class Builder extends GenericDocument.Builder<CustomerDocument.Builder> {
             private Builder(@NonNull String uri) {
                 super(uri, "customerDocument");
             }
 
             @Override
+            @NonNull
             public CustomerDocument build() {
                 return new CustomerDocument(super.build());
             }
diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverterTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverterTest.java
new file mode 100644
index 0000000..a95290d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/GenericDocumentToProtoConverterTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appsearch.external.localbackend.converter;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.GenericDocument;
+
+import com.android.server.appsearch.proto.DocumentProto;
+import com.android.server.appsearch.proto.PropertyProto;
+import com.android.server.appsearch.protobuf.ByteString;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+public class GenericDocumentToProtoConverterTest {
+    private static final byte[] BYTE_ARRAY_1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
+    private static final byte[] BYTE_ARRAY_2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
+    private static final GenericDocument DOCUMENT_PROPERTIES_1 =
+            new GenericDocument.Builder<GenericDocument.Builder<?>>(
+                    "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+            .setCreationTimestampMillis(12345L)
+            .build();
+    private static final GenericDocument DOCUMENT_PROPERTIES_2 =
+            new GenericDocument.Builder<GenericDocument.Builder<?>>(
+                    "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+            .setCreationTimestampMillis(6789L)
+            .build();
+
+    @Test
+    public void testDocumentProtoConvert() {
+        GenericDocument document =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("uri1", "schemaType1")
+                        .setCreationTimestampMillis(5L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setNamespace("namespace")
+                        .setProperty("longKey1", 1L)
+                        .setProperty("doubleKey1", 1.0)
+                        .setProperty("booleanKey1", true)
+                        .setProperty("stringKey1", "test-value1")
+                        .setProperty("byteKey1", BYTE_ARRAY_1, BYTE_ARRAY_2)
+                        .setProperty("documentKey1", DOCUMENT_PROPERTIES_1)
+                        .setProperty(GenericDocument.PROPERTIES_FIELD, DOCUMENT_PROPERTIES_2)
+                        .build();
+
+        // Create the Document proto. Need to sort the property order by key.
+        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+                .setUri("uri1")
+                .setSchema("schemaType1")
+                .setCreationTimestampMs(5L)
+                .setScore(1)
+                .setTtlMs(1L)
+                .setNamespace("namespace");
+        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
+        propertyProtoMap.put("longKey1",
+                PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
+        propertyProtoMap.put("doubleKey1",
+                PropertyProto.newBuilder().setName("doubleKey1").addDoubleValues(1.0));
+        propertyProtoMap.put("booleanKey1",
+                PropertyProto.newBuilder().setName("booleanKey1").addBooleanValues(true));
+        propertyProtoMap.put("stringKey1",
+                PropertyProto.newBuilder().setName("stringKey1").addStringValues("test-value1"));
+        propertyProtoMap.put("byteKey1",
+                PropertyProto.newBuilder().setName("byteKey1")
+                        .addBytesValues(ByteString.copyFrom(BYTE_ARRAY_1))
+                        .addBytesValues(ByteString.copyFrom(BYTE_ARRAY_2)));
+        propertyProtoMap.put("documentKey1",
+                PropertyProto.newBuilder().setName("documentKey1")
+                        .addDocumentValues(
+                                GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_1)));
+        propertyProtoMap.put(GenericDocument.PROPERTIES_FIELD,
+                PropertyProto.newBuilder().setName(GenericDocument.PROPERTIES_FIELD)
+                        .addDocumentValues(
+                                GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_2)));
+        List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
+        Collections.sort(sortedKey);
+        for (String key : sortedKey) {
+            documentProtoBuilder.addProperties(propertyProtoMap.get(key));
+        }
+        DocumentProto documentProto = documentProtoBuilder.build();
+        assertThat(GenericDocumentToProtoConverter.convert(document))
+                .isEqualTo(documentProto);
+        assertThat(document).isEqualTo(GenericDocumentToProtoConverter.convert(documentProto));
+    }
+}
diff --git a/core/tests/coretests/src/android/app/appsearch/SnippetTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SnippetTest.java
similarity index 75%
rename from core/tests/coretests/src/android/app/appsearch/SnippetTest.java
rename to services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SnippetTest.java
index 95f5b10..e9357aa 100644
--- a/core/tests/coretests/src/android/app/appsearch/SnippetTest.java
+++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/converter/SnippetTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,24 +14,23 @@
  * limitations under the License.
  */
 
-package android.app.appsearch;
+package com.android.server.appsearch.external.localbackend.converter;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.appsearch.proto.DocumentProto;
-import android.app.appsearch.proto.PropertyProto;
-import android.app.appsearch.proto.SearchResultProto;
-import android.app.appsearch.proto.SnippetMatchProto;
-import android.app.appsearch.proto.SnippetProto;
+import android.app.appsearch.SearchResult;
 
-import androidx.test.filters.SmallTest;
+import com.android.server.appsearch.proto.DocumentProto;
+import com.android.server.appsearch.proto.PropertyProto;
+import com.android.server.appsearch.proto.SearchResultProto;
+import com.android.server.appsearch.proto.SnippetMatchProto;
+import com.android.server.appsearch.proto.SnippetProto;
 
 import org.junit.Test;
 
-@SmallTest
 public class SnippetTest {
 
-    // TODO(sidchhabra): Add tests for Double and Long Snippets.
+    // TODO(tytytyww): Add tests for Double and Long Snippets.
     @Test
     public void testSingleStringSnippet() {
 
@@ -74,22 +73,26 @@
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
                 .addResults(resultProto)
                 .build();
-        SearchResults searchResults = new SearchResults(searchResultProto);
 
         // Making ResultReader and getting Snippet values.
-        while (searchResults.hasNext()) {
-            SearchResults.Result result = searchResults.next();
-            MatchInfo match = result.getMatchInfo().get(0);
+        for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
+            SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
+            SearchResult.MatchInfo match = result.getMatches().get(0);
             assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
             assertThat(match.getFullText()).isEqualTo(propertyValueString);
             assertThat(match.getExactMatch()).isEqualTo(exactMatch);
+            assertThat(match.getExactMatchPosition()).isEqualTo(
+                    new SearchResult.MatchRange(/*lower=*/29, /*upper=*/32));
+            assertThat(match.getFullText()).isEqualTo(propertyValueString);
+            assertThat(match.getSnippetPosition()).isEqualTo(
+                    new SearchResult.MatchRange(/*lower=*/26, /*upper=*/32));
             assertThat(match.getSnippet()).isEqualTo(window);
         }
     }
 
-    // TODO(sidchhabra): Add tests for Double and Long Snippets.
+    // TODO(tytytyww): Add tests for Double and Long Snippets.
     @Test
-    public void testNoSnippets() {
+    public void testNoSnippets() throws Exception {
 
         final String propertyKeyString = "content";
         final String propertyValueString = "A commonly used fake word is foo.\n"
@@ -117,16 +120,15 @@
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
                 .addResults(resultProto)
                 .build();
-        SearchResults searchResults = new SearchResults(searchResultProto);
 
-        while (searchResults.hasNext()) {
-            SearchResults.Result result = searchResults.next();
-            assertThat(result.getMatchInfo()).isEqualTo(null);
+        for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
+            SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
+            assertThat(result.getMatches()).isEqualTo(null);
         }
     }
 
     @Test
-    public void testMultipleStringSnippet() {
+    public void testMultipleStringSnippet() throws Exception {
         final String searchWord = "Test";
 
         // Building the SearchResult received from query.
@@ -178,22 +180,29 @@
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
                 .addResults(resultProto)
                 .build();
-        SearchResults searchResults = new SearchResults(searchResultProto);
 
         // Making ResultReader and getting Snippet values.
-        while (searchResults.hasNext()) {
-            SearchResults.Result result = searchResults.next();
+        for (SearchResultProto.ResultProto proto : searchResultProto.getResultsList()) {
+            SearchResult result = SearchResultToProtoConverter.convertSearchResult(proto);
 
-            MatchInfo match1 = result.getMatchInfo().get(0);
+            SearchResult.MatchInfo match1 = result.getMatches().get(0);
             assertThat(match1.getPropertyPath()).isEqualTo("sender.name");
             assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
+            assertThat(match1.getExactMatchPosition()).isEqualTo(
+                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4));
             assertThat(match1.getExactMatch()).isEqualTo("Test");
+            assertThat(match1.getSnippetPosition()).isEqualTo(
+                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9));
             assertThat(match1.getSnippet()).isEqualTo("Test Name");
 
-            MatchInfo match2 = result.getMatchInfo().get(1);
+            SearchResult.MatchInfo match2 = result.getMatches().get(1);
             assertThat(match2.getPropertyPath()).isEqualTo("sender.email");
             assertThat(match2.getFullText()).isEqualTo("TestNameJr@gmail.com");
+            assertThat(match2.getExactMatchPosition()).isEqualTo(
+                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
             assertThat(match2.getExactMatch()).isEqualTo("TestNameJr@gmail.com");
+            assertThat(match2.getSnippetPosition()).isEqualTo(
+                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
             assertThat(match2.getSnippet()).isEqualTo("TestNameJr@gmail.com");
         }
     }