[go: nahoru, domu]

Merge commit '0ccdd90e6916f52dbf9d69b0afa7ecea398ad130' into manual_merge_0ccdd90e6916f52dbf9d69b0afa7ecea398ad130

Bug: 193833051
Test: ./gradlew bOS
Change-Id: Idb2e82d38d8ef236c1ce8ba165ce598371309c20
diff --git a/appcompat/appcompat/lint-baseline.xml b/appcompat/appcompat/lint-baseline.xml
index 7a93bc3..f904448 100644
--- a/appcompat/appcompat/lint-baseline.xml
+++ b/appcompat/appcompat/lint-baseline.xml
@@ -6160,7 +6160,7 @@
         errorLine2="                               ~~~~~~">
         <location
             file="src/main/java/androidx/appcompat/widget/AppCompatImageButton.java"
-            line="103"
+            line="112"
             column="32"/>
     </issue>
 
@@ -6171,7 +6171,7 @@
         errorLine2="                                   ~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/appcompat/widget/AppCompatImageHelper.java"
-            line="51"
+            line="52"
             column="36"/>
     </issue>
 
@@ -6182,7 +6182,7 @@
         errorLine2="                               ~~~~~~">
         <location
             file="src/main/java/androidx/appcompat/widget/AppCompatImageView.java"
-            line="113"
+            line="123"
             column="32"/>
     </issue>
 
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
index b532d36..01565c0e 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
@@ -469,8 +469,10 @@
             // after the inserted content (if no space was already present). See
             // https://cs.android.com/android/platform/superproject/+/android-4.4.4_r2:frameworks/base/core/java/android/widget/TextView.java;l=8526,8527,8528,8545,8546
             assertTextAndCursorPosition("ab xz", 2);
-        } else {
+        } else if (Build.VERSION.SDK_INT <= 30) {
             assertTextAndCursorPosition("abxz", 2);
+        } else {
+            assertTextAndCursorPosition("a\nbxz", 3);
         }
     }
 
@@ -610,6 +612,13 @@
             mExtraValue = extraValue;
         }
 
+        @NonNull
+        @Override
+        public String toString() {
+            return "[" + "clip=" + mClip + ", source=" + mSource + ", flags=" + mFlags
+                    + ", linkUri=" + mLinkUri + ", extraValue=" + mExtraValue + "]";
+        }
+
         @Override
         public boolean matches(ContentInfoCompat actual) {
             ClipData.Item expectedItem = mClip.getItemAt(0);
@@ -623,11 +632,22 @@
         }
 
         private boolean extrasMatch(Bundle actualExtras) {
-            if (mSource == SOURCE_INPUT_METHOD && Build.VERSION.SDK_INT >= 25) {
-                assertThat(actualExtras).isNotNull();
+            if (mSource == SOURCE_INPUT_METHOD && Build.VERSION.SDK_INT >= 25
+                    && Build.VERSION.SDK_INT <= 30) {
+                // On SDK >= 25 and <= 30, when inserting from the keyboard, the InputContentInfo
+                // object passed from the IME should be set in the extras. This is needed in order
+                // to prevent premature release of URI permissions. Passing this object via the
+                // extras is not needed on other SDKs: on  SDK < 25, the IME code handles URI
+                // permissions differently (expects the IME to grant URI permissions), and on
+                // SDK > 30, this is handled by the platform implementation of the API.
+                if (actualExtras == null) {
+                    return false;
+                }
                 Parcelable actualInputContentInfoExtra = actualExtras.getParcelable(
                         "androidx.core.view.extra.INPUT_CONTENT_INFO");
-                assertThat(actualInputContentInfoExtra).isInstanceOf(InputContentInfo.class);
+                if (!(actualInputContentInfoExtra instanceof InputContentInfo)) {
+                    return false;
+                }
             } else if (mExtraValue == null) {
                 return actualExtras == null;
             }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
index 7a954d5..708c5ee 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
@@ -230,10 +230,15 @@
         mTextHelper.populateSurroundingTextIfNeeded(this, ic, outAttrs);
         ic = AppCompatHintHelper.onCreateInputConnection(ic, outAttrs, this);
 
-        String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
-        if (ic != null && mimeTypes != null) {
-            EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
-            ic = InputConnectionCompat.createWrapper(this, ic, outAttrs);
+        // On SDK 30 and below, we manually configure the InputConnection here to use
+        // ViewCompat.performReceiveContent. On S and above, the platform's BaseInputConnection
+        // implementation calls View.performReceiveContent by default.
+        if (ic != null && Build.VERSION.SDK_INT <= 30) {
+            String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
+            if (mimeTypes != null) {
+                EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
+                ic = InputConnectionCompat.createWrapper(this, ic, outAttrs);
+            }
         }
         return mAppCompatEmojiEditTextHelper.onCreateInputConnection(ic, outAttrs);
     }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java
index cc84b4b..62d2a49 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java
@@ -33,6 +33,7 @@
 import android.view.View;
 import android.widget.TextView;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -48,15 +49,16 @@
     private static final String LOG_TAG = "ReceiveContent";
 
     /**
-     * If the menu action is either "Paste" or "Paste as plain text" and the view has a
-     * {@link androidx.core.view.OnReceiveContentListener}, use the listener to handle the paste.
+     * If the SDK is <= 30 and the view has a {@link androidx.core.view.OnReceiveContentListener},
+     * use the listener to handle the "Paste" and "Paste as plain text" actions.
      *
      * @return true if the action was handled; false otherwise
      */
     static boolean maybeHandleMenuActionViaPerformReceiveContent(@NonNull TextView view,
-            int menuItemId) {
-        if (!(menuItemId == android.R.id.paste || menuItemId == android.R.id.pasteAsPlainText)
-                || ViewCompat.getOnReceiveContentMimeTypes(view) == null) {
+            int actionId) {
+        if (Build.VERSION.SDK_INT >= 31
+                || ViewCompat.getOnReceiveContentMimeTypes(view) == null
+                || !(actionId == android.R.id.paste || actionId == android.R.id.pasteAsPlainText)) {
             return false;
         }
         ClipboardManager cm = (ClipboardManager) view.getContext().getSystemService(
@@ -64,7 +66,7 @@
         ClipData clip = (cm == null) ? null : cm.getPrimaryClip();
         if (clip != null && clip.getItemCount() > 0) {
             ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
-                    .setFlags((menuItemId == android.R.id.paste) ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT)
+                    .setFlags((actionId == android.R.id.paste) ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT)
                     .build();
             ViewCompat.performReceiveContent(view, payload);
         }
@@ -72,14 +74,16 @@
     }
 
     /**
-     * If the given view has a {@link androidx.core.view.OnReceiveContentListener}, try to handle
-     * drag-and-drop via the listener.
+     * If the SDK is <= 30 (but >= 24) and the view has a
+     * {@link androidx.core.view.OnReceiveContentListener}, try to handle drag-and-drop via the
+     * listener.
      *
      * @return true if the event was handled; false otherwise
      */
     static boolean maybeHandleDragEventViaPerformReceiveContent(@NonNull View view,
             @NonNull DragEvent event) {
-        if (Build.VERSION.SDK_INT < 24
+        if (Build.VERSION.SDK_INT >= 31
+                || Build.VERSION.SDK_INT < 24
                 || event.getLocalState() != null
                 || ViewCompat.getOnReceiveContentMimeTypes(view) == null) {
             return false;
@@ -113,6 +117,7 @@
     private static final class OnDropApi24Impl {
         private OnDropApi24Impl() {}
 
+        @DoNotInline
         static boolean onDropForTextView(@NonNull DragEvent event, @NonNull TextView view,
                 @NonNull Activity activity) {
             activity.requestDragAndDropPermissions(event);
@@ -129,6 +134,7 @@
             return true;
         }
 
+        @DoNotInline
         static boolean onDropForView(@NonNull DragEvent event, @NonNull View view,
                 @NonNull Activity activity) {
             activity.requestDragAndDropPermissions(event);
diff --git a/work/workmanager-gcm/api/2.6.0-beta01.txt b/appsearch/appsearch-ktx/api/current.txt
similarity index 100%
copy from work/workmanager-gcm/api/2.6.0-beta01.txt
copy to appsearch/appsearch-ktx/api/current.txt
diff --git a/work/workmanager-gcm/api/2.6.0-beta01.txt b/appsearch/appsearch-ktx/api/public_plus_experimental_current.txt
similarity index 100%
copy from work/workmanager-gcm/api/2.6.0-beta01.txt
copy to appsearch/appsearch-ktx/api/public_plus_experimental_current.txt
diff --git a/work/workmanager-ktx/api/res-2.6.0-beta01.txt b/appsearch/appsearch-ktx/api/res-current.txt
similarity index 100%
rename from work/workmanager-ktx/api/res-2.6.0-beta01.txt
rename to appsearch/appsearch-ktx/api/res-current.txt
diff --git a/work/workmanager-gcm/api/2.6.0-beta01.txt b/appsearch/appsearch-ktx/api/restricted_current.txt
similarity index 100%
copy from work/workmanager-gcm/api/2.6.0-beta01.txt
copy to appsearch/appsearch-ktx/api/restricted_current.txt
diff --git a/appsearch/appsearch-ktx/build.gradle b/appsearch/appsearch-ktx/build.gradle
new file mode 100644
index 0000000..9675672
--- /dev/null
+++ b/appsearch/appsearch-ktx/build.gradle
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+    id('AndroidXPlugin')
+    id('com.android.library')
+    id('org.jetbrains.kotlin.android')
+    id('org.jetbrains.kotlin.kapt')
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+
+    kaptAndroidTest project(':appsearch:appsearch-compiler')
+    androidTestImplementation project(':appsearch:appsearch')
+    androidTestImplementation project(':appsearch:appsearch-local-storage')
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+}
+
+androidx {
+    name = 'AndroidX AppSearch - Kotlin Extensions'
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.APPSEARCH
+    mavenVersion = LibraryVersions.APPSEARCH
+    inceptionYear = '2021'
+    description = 'AndroidX AppSearch - Kotlin Extensions'
+}
diff --git a/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt b/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
new file mode 100644
index 0000000..e5163dd
--- /dev/null
+++ b/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.ktx
+
+import android.content.Context
+import androidx.appsearch.annotation.Document
+import androidx.appsearch.app.AppSearchSchema
+import androidx.appsearch.app.AppSearchSession
+import androidx.appsearch.app.GenericDocument
+import androidx.appsearch.app.PutDocumentsRequest
+import androidx.appsearch.app.SearchResult
+import androidx.appsearch.app.SearchResults
+import androidx.appsearch.app.SearchSpec
+import androidx.appsearch.app.SetSchemaRequest
+import androidx.appsearch.localstorage.LocalStorage
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+public class AnnotationProcessorKtTest {
+    private companion object {
+        private const val DB_NAME = ""
+    }
+
+    private lateinit var session: AppSearchSession
+
+    @Before
+    public fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        session = LocalStorage.createSearchSession(
+            LocalStorage.SearchContext.Builder(context, DB_NAME).build()
+        ).get()
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        cleanup()
+    }
+
+    @After
+    public fun tearDown() {
+        // Cleanup whatever documents may still exist in these databases.
+        cleanup()
+    }
+
+    private fun cleanup() {
+        session.setSchema(SetSchemaRequest.Builder().setForceOverride(true).build()).get()
+    }
+
+    @Document
+    internal data class Card(
+        @Document.Namespace
+        val namespace: String,
+
+        @Document.Id
+        val id: String,
+
+        @Document.CreationTimestampMillis
+        val creationTimestampMillis: Long = 0L,
+
+        @Document.StringProperty(
+            indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES,
+            tokenizerType = AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN
+        )
+        val string: String? = null,
+    )
+
+    @Document
+    internal data class Gift(
+        @Document.Namespace
+        val namespace: String,
+
+        @Document.Id
+        val id: String,
+
+        @Document.CreationTimestampMillis
+        val creationTimestampMillis: Long = 0L,
+
+        // Collections
+        @Document.LongProperty
+        val collectLong: Collection<Long>,
+
+        @Document.LongProperty
+        val collectInteger: Collection<Int>,
+
+        @Document.DoubleProperty
+        val collectDouble: Collection<Double>,
+
+        @Document.DoubleProperty
+        val collectFloat: Collection<Float>,
+
+        @Document.BooleanProperty
+        val collectBoolean: Collection<Boolean>,
+
+        @Document.BytesProperty
+        val collectByteArr: Collection<ByteArray>,
+
+        @Document.StringProperty
+        val collectString: Collection<String>,
+
+        @Document.DocumentProperty
+        val collectCard: Collection<Card>,
+
+        // Arrays
+        @Document.LongProperty
+        val arrBoxLong: Array<Long>,
+
+        @Document.LongProperty
+        val arrUnboxLong: LongArray,
+
+        @Document.LongProperty
+        val arrBoxInteger: Array<Int>,
+
+        @Document.LongProperty
+        val arrUnboxInt: IntArray,
+
+        @Document.DoubleProperty
+        val arrBoxDouble: Array<Double>,
+
+        @Document.DoubleProperty
+        val arrUnboxDouble: DoubleArray,
+
+        @Document.DoubleProperty
+        val arrBoxFloat: Array<Float>,
+
+        @Document.DoubleProperty
+        val arrUnboxFloat: FloatArray,
+
+        @Document.BooleanProperty
+        val arrBoxBoolean: Array<Boolean>,
+
+        @Document.BooleanProperty
+        val arrUnboxBoolean: BooleanArray,
+
+        @Document.BytesProperty
+        val arrUnboxByteArr: Array<ByteArray>,
+
+        @Document.BytesProperty
+        val boxByteArr: Array<Byte>,
+
+        @Document.StringProperty
+        val arrString: Array<String>,
+
+        @Document.DocumentProperty
+        val arrCard: Array<Card>,
+
+        // Single values
+        @Document.StringProperty
+        val string: String,
+
+        @Document.LongProperty
+        val boxLong: Long,
+
+        @Document.LongProperty
+        val unboxLong: Long = 0,
+
+        @Document.LongProperty
+        val boxInteger: Int,
+
+        @Document.LongProperty
+        val unboxInt: Int = 0,
+
+        @Document.DoubleProperty
+        val boxDouble: Double,
+
+        @Document.DoubleProperty
+        val unboxDouble: Double = 0.0,
+
+        @Document.DoubleProperty
+        val boxFloat: Float,
+
+        @Document.DoubleProperty
+        val unboxFloat: Float = 0f,
+
+        @Document.BooleanProperty
+        val boxBoolean: Boolean,
+
+        @Document.BooleanProperty
+        val unboxBoolean: Boolean = false,
+
+        @Document.BytesProperty
+        val unboxByteArr: ByteArray,
+
+        @Document.DocumentProperty
+        val card: Card
+    ) {
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (javaClass != other?.javaClass) return false
+
+            other as Gift
+
+            if (namespace != other.namespace) return false
+            if (id != other.id) return false
+            if (collectLong != other.collectLong) return false
+            if (collectInteger != other.collectInteger) return false
+            if (collectDouble != other.collectDouble) return false
+            if (collectFloat != other.collectFloat) return false
+            if (collectBoolean != other.collectBoolean) return false
+            // It's complicated to do a deep comparison of this, so we skip it
+            // if (collectByteArr != other.collectByteArr) return false
+            if (collectString != other.collectString) return false
+            if (collectCard != other.collectCard) return false
+            if (!arrBoxLong.contentEquals(other.arrBoxLong)) return false
+            if (!arrUnboxLong.contentEquals(other.arrUnboxLong)) return false
+            if (!arrBoxInteger.contentEquals(other.arrBoxInteger)) return false
+            if (!arrUnboxInt.contentEquals(other.arrUnboxInt)) return false
+            if (!arrBoxDouble.contentEquals(other.arrBoxDouble)) return false
+            if (!arrUnboxDouble.contentEquals(other.arrUnboxDouble)) return false
+            if (!arrBoxFloat.contentEquals(other.arrBoxFloat)) return false
+            if (!arrUnboxFloat.contentEquals(other.arrUnboxFloat)) return false
+            if (!arrBoxBoolean.contentEquals(other.arrBoxBoolean)) return false
+            if (!arrUnboxBoolean.contentEquals(other.arrUnboxBoolean)) return false
+            if (!arrUnboxByteArr.contentDeepEquals(other.arrUnboxByteArr)) return false
+            if (!boxByteArr.contentEquals(other.boxByteArr)) return false
+            if (!arrString.contentEquals(other.arrString)) return false
+            if (!arrCard.contentEquals(other.arrCard)) return false
+            if (string != other.string) return false
+            if (boxLong != other.boxLong) return false
+            if (unboxLong != other.unboxLong) return false
+            if (boxInteger != other.boxInteger) return false
+            if (unboxInt != other.unboxInt) return false
+            if (boxDouble != other.boxDouble) return false
+            if (unboxDouble != other.unboxDouble) return false
+            if (boxFloat != other.boxFloat) return false
+            if (unboxFloat != other.unboxFloat) return false
+            if (boxBoolean != other.boxBoolean) return false
+            if (unboxBoolean != other.unboxBoolean) return false
+            if (!unboxByteArr.contentEquals(other.unboxByteArr)) return false
+            if (card != other.card) return false
+
+            return true
+        }
+    }
+
+    @Test
+    public fun testAnnotationProcessor() {
+        session.setSchema(
+            SetSchemaRequest.Builder()
+                .addDocumentClasses(Card::class.java, Gift::class.java).build()
+        ).get()
+
+        // Create a Gift object and assign values.
+        val inputDocument = createPopulatedGift()
+
+        // Index the Gift document and query it.
+        session.put(PutDocumentsRequest.Builder().addDocuments(inputDocument).build())
+            .get().checkSuccess()
+        val searchResults = session.search("", SearchSpec.Builder().build())
+        val documents = convertSearchResultsToDocuments(searchResults)
+        assertThat(documents).hasSize(1)
+
+        // Convert GenericDocument to Gift and check values.
+        val outputDocument = documents[0].toDocumentClass(Gift::class.java)
+        assertThat(outputDocument).isEqualTo(inputDocument)
+    }
+
+    @Test
+    public fun testGenericDocumentConversion() {
+        val inGift = createPopulatedGift()
+        val genericDocument1 = GenericDocument.fromDocumentClass(inGift)
+        val genericDocument2 = GenericDocument.fromDocumentClass(inGift)
+        val outGift = genericDocument2.toDocumentClass(Gift::class.java)
+        assertThat(inGift).isNotSameInstanceAs(outGift)
+        assertThat(inGift).isEqualTo(outGift)
+        assertThat(genericDocument1).isNotSameInstanceAs(genericDocument2)
+        assertThat(genericDocument1).isEqualTo(genericDocument2)
+    }
+
+    private fun createPopulatedGift(): Gift {
+        val card1 = Card("card.namespace", "card.id1")
+        val card2 = Card("card.namespace", "card.id2")
+        return Gift(
+            namespace = "gift.namespace",
+            id = "gift.id",
+            arrBoxBoolean = arrayOf(true, false),
+            arrBoxDouble = arrayOf(0.0, 1.0),
+            arrBoxFloat = arrayOf(2.0f, 3.0f),
+            arrBoxInteger = arrayOf(4, 5),
+            arrBoxLong = arrayOf(6L, 7L),
+            arrString = arrayOf("cat", "dog"),
+            boxByteArr = arrayOf(8, 9),
+            arrUnboxBoolean = booleanArrayOf(false, true),
+            arrUnboxByteArr = arrayOf(byteArrayOf(0, 1), byteArrayOf(2, 3)),
+            arrUnboxDouble = doubleArrayOf(1.0, 0.0),
+            arrUnboxFloat = floatArrayOf(3.0f, 2.0f),
+            arrUnboxInt = intArrayOf(5, 4),
+            arrUnboxLong = longArrayOf(7, 6),
+            arrCard = arrayOf(card2, card2),
+            collectLong = listOf(6L, 7L),
+            collectInteger = listOf(4, 5),
+            collectBoolean = listOf(false, true),
+            collectString = listOf("cat", "dog"),
+            collectDouble = listOf(0.0, 1.0),
+            collectFloat = listOf(2.0f, 3.0f),
+            collectByteArr = listOf(byteArrayOf(0, 1), byteArrayOf(2, 3)),
+            collectCard = listOf(card2, card2),
+            string = "String",
+            boxLong = 1L,
+            unboxLong = 2L,
+            boxInteger = 3,
+            unboxInt = 4,
+            boxDouble = 5.0,
+            unboxDouble = 6.0,
+            boxFloat = 7.0f,
+            unboxFloat = 8.0f,
+            boxBoolean = true,
+            unboxBoolean = false,
+            unboxByteArr = byteArrayOf(1, 2, 3),
+            card = card1
+        )
+    }
+
+    private fun convertSearchResultsToDocuments(
+        searchResults: SearchResults
+    ): List<GenericDocument> {
+        var page = searchResults.nextPage.get()
+        val results = mutableListOf<SearchResult>()
+        while (page.isNotEmpty()) {
+            results.addAll(page)
+            page = searchResults.nextPage.get()
+        }
+        return results.map { it.genericDocument }
+    }
+}
diff --git a/appsearch/appsearch-ktx/src/main/AndroidManifest.xml b/appsearch/appsearch-ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1c35209
--- /dev/null
+++ b/appsearch/appsearch-ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2021 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.
+-->
+
+<manifest package="androidx.appsearch.ktx"/>
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index de75467..b5282d0 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -1,30 +1,56 @@
 // Signature format: 4.0
 package androidx.appsearch.annotation {
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface AppSearchDocument {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface Document {
     method public abstract String name() default "";
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.CreationTimestampMillis {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Namespace {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Property {
-    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.BooleanProperty {
     method public abstract String name() default "";
     method public abstract boolean required() default false;
-    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Score {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.BytesProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.TtlMillis {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.CreationTimestampMillis {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Uri {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.DocumentProperty {
+    method public abstract boolean indexNestedProperties() default false;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.DoubleProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.LongProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Namespace {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Score {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.StringProperty {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.TtlMillis {
   }
 
 }
@@ -32,16 +58,27 @@
 package androidx.appsearch.app {
 
   public final class AppSearchBatchResult<KeyType, ValueType> {
+    method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getAll();
     method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getFailures();
     method public java.util.Map<KeyType!,ValueType!> getSuccesses();
     method public boolean isSuccess();
   }
 
+  public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
+    ctor public AppSearchBatchResult.Builder();
+    method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
+  }
+
   public final class AppSearchResult<ValueType> {
     method public String? getErrorMessage();
     method public int getResultCode();
     method public ValueType? getResultValue();
     method public boolean isSuccess();
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
     field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
     field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
     field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -49,6 +86,7 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
@@ -57,28 +95,71 @@
     method public String getSchemaType();
   }
 
+  public static final class AppSearchSchema.BooleanPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BooleanPropertyConfig.Builder {
+    ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+  }
+
   public static final class AppSearchSchema.Builder {
     ctor public AppSearchSchema.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
   }
 
-  public static final class AppSearchSchema.PropertyConfig {
+  public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BytesPropertyConfig.Builder {
+    ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public String getSchemaType();
+    method public boolean shouldIndexNestedProperties();
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig.Builder {
+    ctor public AppSearchSchema.DocumentPropertyConfig.Builder(String, String);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig.Builder {
+    ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.LongPropertyConfig.Builder {
+    ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+  }
+
+  public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
-    method public int getDataType();
-    method public int getIndexingType();
     method public String getName();
-    method public String? getSchemaType();
-    method public int getTokenizerType();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
     field public static final int CARDINALITY_REQUIRED = 3; // 0x3
-    field public static final int DATA_TYPE_BOOLEAN = 4; // 0x4
-    field public static final int DATA_TYPE_BYTES = 5; // 0x5
-    field public static final int DATA_TYPE_DOCUMENT = 6; // 0x6
-    field public static final int DATA_TYPE_DOUBLE = 3; // 0x3
-    field public static final int DATA_TYPE_INT64 = 2; // 0x2
-    field public static final int DATA_TYPE_STRING = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    method public int getTokenizerType();
     field public static final int INDEXING_TYPE_EXACT_TERMS = 1; // 0x1
     field public static final int INDEXING_TYPE_NONE = 0; // 0x0
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
@@ -86,37 +167,41 @@
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
   }
 
-  public static final class AppSearchSchema.PropertyConfig.Builder {
-    ctor public AppSearchSchema.PropertyConfig.Builder(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig build();
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setCardinality(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setDataType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setIndexingType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setSchemaType(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setTokenizerType(int);
+  public static final class AppSearchSchema.StringPropertyConfig.Builder {
+    ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
   }
 
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!> getSchema();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeByUri(androidx.appsearch.app.RemoveByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface DataClassFactory<T> {
+  public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
-    method public String getSchemaType();
+    method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
+    method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public long getCreationTimestampMillis();
+    method public String getId();
     method public static int getMaxIndexedProperties();
     method public String getNamespace();
     method public Object? getProperty(String);
@@ -136,15 +221,16 @@
     method public String getSchemaType();
     method public int getScore();
     method public long getTtlMillis();
-    method public String getUri();
-    method public <T> T toDataClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    field public static final String DEFAULT_NAMESPACE = "";
+    method public androidx.appsearch.app.GenericDocument.Builder<androidx.appsearch.app.GenericDocument.Builder<?>!> toBuilder();
+    method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
-    ctor public GenericDocument.Builder(String, String);
+    ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
+    method public BuilderType clearProperty(String);
     method public BuilderType setCreationTimestampMillis(long);
+    method public BuilderType setId(String);
     method public BuilderType setNamespace(String);
     method public BuilderType setPropertyBoolean(String, boolean...);
     method public BuilderType setPropertyBytes(String, byte[]!...);
@@ -152,25 +238,49 @@
     method public BuilderType setPropertyDouble(String, double...);
     method public BuilderType setPropertyLong(String, long...);
     method public BuilderType setPropertyString(String, java.lang.String!...);
+    method public BuilderType setSchemaType(String);
     method public BuilderType setScore(@IntRange(from=0, to=java.lang.Integer.MAX_VALUE) int);
     method public BuilderType setTtlMillis(long);
   }
 
-  public final class GetByUriRequest {
+  public final class GetByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
   }
 
-  public static final class GetByUriRequest.Builder {
-    ctor public GetByUriRequest.Builder();
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.GetByUriRequest build();
-    method public androidx.appsearch.app.GetByUriRequest.Builder setNamespace(String);
+  public static final class GetByDocumentIdRequest.Builder {
+    ctor public GetByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addProjection(String, java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest build();
   }
 
-  public interface GlobalSearchSession {
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
+  public final class GetSchemaResponse {
+    method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
+    method @IntRange(from=0) public int getVersion();
+  }
+
+  public static final class GetSchemaResponse.Builder {
+    ctor public GetSchemaResponse.Builder();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+    method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
+  }
+
+  public interface GlobalSearchSession extends java.io.Closeable {
+    method public void close();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
+  public abstract class Migrator {
+    ctor public Migrator();
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onDowngrade(int, int, androidx.appsearch.app.GenericDocument);
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onUpgrade(int, int, androidx.appsearch.app.GenericDocument);
+    method public abstract boolean shouldMigrate(int, int);
   }
 
   public class PackageIdentifier {
@@ -180,47 +290,92 @@
   }
 
   public final class PutDocumentsRequest {
-    method public java.util.List<androidx.appsearch.app.GenericDocument!> getDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
     ctor public PutDocumentsRequest.Builder();
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(androidx.appsearch.app.GenericDocument!...);
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
-  public final class RemoveByUriRequest {
+  public final class RemoveByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
   }
 
-  public static final class RemoveByUriRequest.Builder {
-    ctor public RemoveByUriRequest.Builder();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.RemoveByUriRequest build();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder setNamespace(String);
+  public static final class RemoveByDocumentIdRequest.Builder {
+    ctor public RemoveByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest build();
+  }
+
+  public final class ReportSystemUsageRequest {
+    method public String getDatabaseName();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getPackageName();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportSystemUsageRequest.Builder {
+    ctor public ReportSystemUsageRequest.Builder(String, String, String, String);
+    method public androidx.appsearch.app.ReportSystemUsageRequest build();
+    method public androidx.appsearch.app.ReportSystemUsageRequest.Builder setUsageTimestampMillis(long);
+  }
+
+  public final class ReportUsageRequest {
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportUsageRequest.Builder {
+    ctor public ReportUsageRequest.Builder(String, String);
+    method public androidx.appsearch.app.ReportUsageRequest build();
+    method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
   public final class SearchResult {
-    method public androidx.appsearch.app.GenericDocument getDocument();
-    method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatches();
+    method public String getDatabaseName();
+    method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.GenericDocument getGenericDocument();
+    method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
     method public String getPackageName();
+    method public double getRankingSignal();
+  }
+
+  public static final class SearchResult.Builder {
+    ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
+    method public androidx.appsearch.app.SearchResult build();
+    method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
   public static final class SearchResult.MatchInfo {
     method public CharSequence getExactMatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchRange();
     method public String getFullText();
     method public String getPropertyPath();
     method public CharSequence getSnippet();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
+  }
+
+  public static final class SearchResult.MatchInfo.Builder {
+    ctor public SearchResult.MatchInfo.Builder(String);
+    method public androidx.appsearch.app.SearchResult.MatchInfo build();
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setExactMatchRange(androidx.appsearch.app.SearchResult.MatchRange);
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setSnippetRange(androidx.appsearch.app.SearchResult.MatchRange);
   }
 
   public static final class SearchResult.MatchRange {
+    ctor public SearchResult.MatchRange(int, int);
     method public int getEnd();
     method public int getStart();
   }
@@ -231,16 +386,21 @@
   }
 
   public final class SearchSpec {
+    method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaxSnippetSize();
-    method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
-    method public java.util.List<java.lang.String!> getSchemaTypes();
+    method public int getResultGroupingLimit();
+    method public int getResultGroupingTypeFlags();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
+    field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
     field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
@@ -248,49 +408,102 @@
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
     field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; // 0x6
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
+    field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
+    field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
 
   public static final class SearchSpec.Builder {
     ctor public SearchSpec.Builder();
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
+    method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
   }
 
   public final class SetSchemaRequest {
+    method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
-    method public java.util.Set<java.lang.String!> getSchemasNotVisibleToSystemUi();
+    method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
     method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
+    method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(androidx.appsearch.app.AppSearchSchema!...);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
     method public androidx.appsearch.app.SetSchemaRequest build();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForSystemUi(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForSystemUi(String, boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
+  }
+
+  public class SetSchemaResponse {
+    method public java.util.Set<java.lang.String!> getDeletedTypes();
+    method public java.util.Set<java.lang.String!> getIncompatibleTypes();
+    method public java.util.Set<java.lang.String!> getMigratedTypes();
+    method public java.util.List<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!> getMigrationFailures();
+  }
+
+  public static final class SetSchemaResponse.Builder {
+    ctor public SetSchemaResponse.Builder();
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailure(androidx.appsearch.app.SetSchemaResponse.MigrationFailure);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailures(java.util.Collection<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!>);
+    method public androidx.appsearch.app.SetSchemaResponse build();
+  }
+
+  public static class SetSchemaResponse.MigrationFailure {
+    ctor public SetSchemaResponse.MigrationFailure(String, String, String, androidx.appsearch.app.AppSearchResult<?>);
+    method public androidx.appsearch.app.AppSearchResult<java.lang.Void!> getAppSearchResult();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getSchemaType();
+  }
+
+  public class StorageInfo {
+    method public int getAliveDocumentsCount();
+    method public int getAliveNamespacesCount();
+    method public long getSizeBytes();
+  }
+
+  public static final class StorageInfo.Builder {
+    ctor public StorageInfo.Builder();
+    method public androidx.appsearch.app.StorageInfo build();
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
   }
 
 }
@@ -298,6 +511,9 @@
 package androidx.appsearch.exceptions {
 
   public class AppSearchException extends java.lang.Exception {
+    ctor public AppSearchException(int);
+    ctor public AppSearchException(int, String?);
+    ctor public AppSearchException(int, String?, Throwable?);
     method public int getResultCode();
     method public <T> androidx.appsearch.app.AppSearchResult<T!> toAppSearchResult();
   }
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index de75467..b5282d0 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -1,30 +1,56 @@
 // Signature format: 4.0
 package androidx.appsearch.annotation {
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface AppSearchDocument {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface Document {
     method public abstract String name() default "";
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.CreationTimestampMillis {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Namespace {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Property {
-    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.BooleanProperty {
     method public abstract String name() default "";
     method public abstract boolean required() default false;
-    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Score {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.BytesProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.TtlMillis {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.CreationTimestampMillis {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Uri {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.DocumentProperty {
+    method public abstract boolean indexNestedProperties() default false;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.DoubleProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.LongProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Namespace {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Score {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.StringProperty {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.TtlMillis {
   }
 
 }
@@ -32,16 +58,27 @@
 package androidx.appsearch.app {
 
   public final class AppSearchBatchResult<KeyType, ValueType> {
+    method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getAll();
     method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getFailures();
     method public java.util.Map<KeyType!,ValueType!> getSuccesses();
     method public boolean isSuccess();
   }
 
+  public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
+    ctor public AppSearchBatchResult.Builder();
+    method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
+  }
+
   public final class AppSearchResult<ValueType> {
     method public String? getErrorMessage();
     method public int getResultCode();
     method public ValueType? getResultValue();
     method public boolean isSuccess();
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
     field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
     field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
     field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -49,6 +86,7 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
@@ -57,28 +95,71 @@
     method public String getSchemaType();
   }
 
+  public static final class AppSearchSchema.BooleanPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BooleanPropertyConfig.Builder {
+    ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+  }
+
   public static final class AppSearchSchema.Builder {
     ctor public AppSearchSchema.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
   }
 
-  public static final class AppSearchSchema.PropertyConfig {
+  public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BytesPropertyConfig.Builder {
+    ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public String getSchemaType();
+    method public boolean shouldIndexNestedProperties();
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig.Builder {
+    ctor public AppSearchSchema.DocumentPropertyConfig.Builder(String, String);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig.Builder {
+    ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.LongPropertyConfig.Builder {
+    ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+  }
+
+  public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
-    method public int getDataType();
-    method public int getIndexingType();
     method public String getName();
-    method public String? getSchemaType();
-    method public int getTokenizerType();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
     field public static final int CARDINALITY_REQUIRED = 3; // 0x3
-    field public static final int DATA_TYPE_BOOLEAN = 4; // 0x4
-    field public static final int DATA_TYPE_BYTES = 5; // 0x5
-    field public static final int DATA_TYPE_DOCUMENT = 6; // 0x6
-    field public static final int DATA_TYPE_DOUBLE = 3; // 0x3
-    field public static final int DATA_TYPE_INT64 = 2; // 0x2
-    field public static final int DATA_TYPE_STRING = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    method public int getTokenizerType();
     field public static final int INDEXING_TYPE_EXACT_TERMS = 1; // 0x1
     field public static final int INDEXING_TYPE_NONE = 0; // 0x0
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
@@ -86,37 +167,41 @@
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
   }
 
-  public static final class AppSearchSchema.PropertyConfig.Builder {
-    ctor public AppSearchSchema.PropertyConfig.Builder(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig build();
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setCardinality(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setDataType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setIndexingType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setSchemaType(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setTokenizerType(int);
+  public static final class AppSearchSchema.StringPropertyConfig.Builder {
+    ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
   }
 
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!> getSchema();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeByUri(androidx.appsearch.app.RemoveByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface DataClassFactory<T> {
+  public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
-    method public String getSchemaType();
+    method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
+    method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public long getCreationTimestampMillis();
+    method public String getId();
     method public static int getMaxIndexedProperties();
     method public String getNamespace();
     method public Object? getProperty(String);
@@ -136,15 +221,16 @@
     method public String getSchemaType();
     method public int getScore();
     method public long getTtlMillis();
-    method public String getUri();
-    method public <T> T toDataClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    field public static final String DEFAULT_NAMESPACE = "";
+    method public androidx.appsearch.app.GenericDocument.Builder<androidx.appsearch.app.GenericDocument.Builder<?>!> toBuilder();
+    method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
-    ctor public GenericDocument.Builder(String, String);
+    ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
+    method public BuilderType clearProperty(String);
     method public BuilderType setCreationTimestampMillis(long);
+    method public BuilderType setId(String);
     method public BuilderType setNamespace(String);
     method public BuilderType setPropertyBoolean(String, boolean...);
     method public BuilderType setPropertyBytes(String, byte[]!...);
@@ -152,25 +238,49 @@
     method public BuilderType setPropertyDouble(String, double...);
     method public BuilderType setPropertyLong(String, long...);
     method public BuilderType setPropertyString(String, java.lang.String!...);
+    method public BuilderType setSchemaType(String);
     method public BuilderType setScore(@IntRange(from=0, to=java.lang.Integer.MAX_VALUE) int);
     method public BuilderType setTtlMillis(long);
   }
 
-  public final class GetByUriRequest {
+  public final class GetByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
   }
 
-  public static final class GetByUriRequest.Builder {
-    ctor public GetByUriRequest.Builder();
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.GetByUriRequest build();
-    method public androidx.appsearch.app.GetByUriRequest.Builder setNamespace(String);
+  public static final class GetByDocumentIdRequest.Builder {
+    ctor public GetByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addProjection(String, java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest build();
   }
 
-  public interface GlobalSearchSession {
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
+  public final class GetSchemaResponse {
+    method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
+    method @IntRange(from=0) public int getVersion();
+  }
+
+  public static final class GetSchemaResponse.Builder {
+    ctor public GetSchemaResponse.Builder();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+    method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
+  }
+
+  public interface GlobalSearchSession extends java.io.Closeable {
+    method public void close();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
+  public abstract class Migrator {
+    ctor public Migrator();
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onDowngrade(int, int, androidx.appsearch.app.GenericDocument);
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onUpgrade(int, int, androidx.appsearch.app.GenericDocument);
+    method public abstract boolean shouldMigrate(int, int);
   }
 
   public class PackageIdentifier {
@@ -180,47 +290,92 @@
   }
 
   public final class PutDocumentsRequest {
-    method public java.util.List<androidx.appsearch.app.GenericDocument!> getDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
     ctor public PutDocumentsRequest.Builder();
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(androidx.appsearch.app.GenericDocument!...);
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
-  public final class RemoveByUriRequest {
+  public final class RemoveByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
   }
 
-  public static final class RemoveByUriRequest.Builder {
-    ctor public RemoveByUriRequest.Builder();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.RemoveByUriRequest build();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder setNamespace(String);
+  public static final class RemoveByDocumentIdRequest.Builder {
+    ctor public RemoveByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest build();
+  }
+
+  public final class ReportSystemUsageRequest {
+    method public String getDatabaseName();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getPackageName();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportSystemUsageRequest.Builder {
+    ctor public ReportSystemUsageRequest.Builder(String, String, String, String);
+    method public androidx.appsearch.app.ReportSystemUsageRequest build();
+    method public androidx.appsearch.app.ReportSystemUsageRequest.Builder setUsageTimestampMillis(long);
+  }
+
+  public final class ReportUsageRequest {
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportUsageRequest.Builder {
+    ctor public ReportUsageRequest.Builder(String, String);
+    method public androidx.appsearch.app.ReportUsageRequest build();
+    method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
   public final class SearchResult {
-    method public androidx.appsearch.app.GenericDocument getDocument();
-    method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatches();
+    method public String getDatabaseName();
+    method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.GenericDocument getGenericDocument();
+    method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
     method public String getPackageName();
+    method public double getRankingSignal();
+  }
+
+  public static final class SearchResult.Builder {
+    ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
+    method public androidx.appsearch.app.SearchResult build();
+    method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
   public static final class SearchResult.MatchInfo {
     method public CharSequence getExactMatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchRange();
     method public String getFullText();
     method public String getPropertyPath();
     method public CharSequence getSnippet();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
+  }
+
+  public static final class SearchResult.MatchInfo.Builder {
+    ctor public SearchResult.MatchInfo.Builder(String);
+    method public androidx.appsearch.app.SearchResult.MatchInfo build();
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setExactMatchRange(androidx.appsearch.app.SearchResult.MatchRange);
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setSnippetRange(androidx.appsearch.app.SearchResult.MatchRange);
   }
 
   public static final class SearchResult.MatchRange {
+    ctor public SearchResult.MatchRange(int, int);
     method public int getEnd();
     method public int getStart();
   }
@@ -231,16 +386,21 @@
   }
 
   public final class SearchSpec {
+    method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaxSnippetSize();
-    method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
-    method public java.util.List<java.lang.String!> getSchemaTypes();
+    method public int getResultGroupingLimit();
+    method public int getResultGroupingTypeFlags();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
+    field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
     field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
@@ -248,49 +408,102 @@
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
     field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; // 0x6
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
+    field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
+    field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
 
   public static final class SearchSpec.Builder {
     ctor public SearchSpec.Builder();
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
+    method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
   }
 
   public final class SetSchemaRequest {
+    method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
-    method public java.util.Set<java.lang.String!> getSchemasNotVisibleToSystemUi();
+    method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
     method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
+    method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(androidx.appsearch.app.AppSearchSchema!...);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
     method public androidx.appsearch.app.SetSchemaRequest build();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForSystemUi(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForSystemUi(String, boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
+  }
+
+  public class SetSchemaResponse {
+    method public java.util.Set<java.lang.String!> getDeletedTypes();
+    method public java.util.Set<java.lang.String!> getIncompatibleTypes();
+    method public java.util.Set<java.lang.String!> getMigratedTypes();
+    method public java.util.List<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!> getMigrationFailures();
+  }
+
+  public static final class SetSchemaResponse.Builder {
+    ctor public SetSchemaResponse.Builder();
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailure(androidx.appsearch.app.SetSchemaResponse.MigrationFailure);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailures(java.util.Collection<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!>);
+    method public androidx.appsearch.app.SetSchemaResponse build();
+  }
+
+  public static class SetSchemaResponse.MigrationFailure {
+    ctor public SetSchemaResponse.MigrationFailure(String, String, String, androidx.appsearch.app.AppSearchResult<?>);
+    method public androidx.appsearch.app.AppSearchResult<java.lang.Void!> getAppSearchResult();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getSchemaType();
+  }
+
+  public class StorageInfo {
+    method public int getAliveDocumentsCount();
+    method public int getAliveNamespacesCount();
+    method public long getSizeBytes();
+  }
+
+  public static final class StorageInfo.Builder {
+    ctor public StorageInfo.Builder();
+    method public androidx.appsearch.app.StorageInfo build();
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
   }
 
 }
@@ -298,6 +511,9 @@
 package androidx.appsearch.exceptions {
 
   public class AppSearchException extends java.lang.Exception {
+    ctor public AppSearchException(int);
+    ctor public AppSearchException(int, String?);
+    ctor public AppSearchException(int, String?, Throwable?);
     method public int getResultCode();
     method public <T> androidx.appsearch.app.AppSearchResult<T!> toAppSearchResult();
   }
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index de75467..b5282d0 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -1,30 +1,56 @@
 // Signature format: 4.0
 package androidx.appsearch.annotation {
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface AppSearchDocument {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface Document {
     method public abstract String name() default "";
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.CreationTimestampMillis {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Namespace {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Property {
-    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.BooleanProperty {
     method public abstract String name() default "";
     method public abstract boolean required() default false;
-    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Score {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.BytesProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.TtlMillis {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.CreationTimestampMillis {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Uri {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.DocumentProperty {
+    method public abstract boolean indexNestedProperties() default false;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.DoubleProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.LongProperty {
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Namespace {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Score {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.StringProperty {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.TtlMillis {
   }
 
 }
@@ -32,16 +58,27 @@
 package androidx.appsearch.app {
 
   public final class AppSearchBatchResult<KeyType, ValueType> {
+    method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getAll();
     method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getFailures();
     method public java.util.Map<KeyType!,ValueType!> getSuccesses();
     method public boolean isSuccess();
   }
 
+  public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
+    ctor public AppSearchBatchResult.Builder();
+    method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
+  }
+
   public final class AppSearchResult<ValueType> {
     method public String? getErrorMessage();
     method public int getResultCode();
     method public ValueType? getResultValue();
     method public boolean isSuccess();
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
     field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
     field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
     field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -49,6 +86,7 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
@@ -57,28 +95,71 @@
     method public String getSchemaType();
   }
 
+  public static final class AppSearchSchema.BooleanPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BooleanPropertyConfig.Builder {
+    ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+  }
+
   public static final class AppSearchSchema.Builder {
     ctor public AppSearchSchema.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
   }
 
-  public static final class AppSearchSchema.PropertyConfig {
+  public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BytesPropertyConfig.Builder {
+    ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public String getSchemaType();
+    method public boolean shouldIndexNestedProperties();
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig.Builder {
+    ctor public AppSearchSchema.DocumentPropertyConfig.Builder(String, String);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig.Builder {
+    ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.LongPropertyConfig.Builder {
+    ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+  }
+
+  public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
-    method public int getDataType();
-    method public int getIndexingType();
     method public String getName();
-    method public String? getSchemaType();
-    method public int getTokenizerType();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
     field public static final int CARDINALITY_REQUIRED = 3; // 0x3
-    field public static final int DATA_TYPE_BOOLEAN = 4; // 0x4
-    field public static final int DATA_TYPE_BYTES = 5; // 0x5
-    field public static final int DATA_TYPE_DOCUMENT = 6; // 0x6
-    field public static final int DATA_TYPE_DOUBLE = 3; // 0x3
-    field public static final int DATA_TYPE_INT64 = 2; // 0x2
-    field public static final int DATA_TYPE_STRING = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    method public int getTokenizerType();
     field public static final int INDEXING_TYPE_EXACT_TERMS = 1; // 0x1
     field public static final int INDEXING_TYPE_NONE = 0; // 0x0
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
@@ -86,37 +167,41 @@
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
   }
 
-  public static final class AppSearchSchema.PropertyConfig.Builder {
-    ctor public AppSearchSchema.PropertyConfig.Builder(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig build();
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setCardinality(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setDataType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setIndexingType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setSchemaType(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setTokenizerType(int);
+  public static final class AppSearchSchema.StringPropertyConfig.Builder {
+    ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
   }
 
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!> getSchema();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeByUri(androidx.appsearch.app.RemoveByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface DataClassFactory<T> {
+  public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
-    method public String getSchemaType();
+    method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
+    method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public long getCreationTimestampMillis();
+    method public String getId();
     method public static int getMaxIndexedProperties();
     method public String getNamespace();
     method public Object? getProperty(String);
@@ -136,15 +221,16 @@
     method public String getSchemaType();
     method public int getScore();
     method public long getTtlMillis();
-    method public String getUri();
-    method public <T> T toDataClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    field public static final String DEFAULT_NAMESPACE = "";
+    method public androidx.appsearch.app.GenericDocument.Builder<androidx.appsearch.app.GenericDocument.Builder<?>!> toBuilder();
+    method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
-    ctor public GenericDocument.Builder(String, String);
+    ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
+    method public BuilderType clearProperty(String);
     method public BuilderType setCreationTimestampMillis(long);
+    method public BuilderType setId(String);
     method public BuilderType setNamespace(String);
     method public BuilderType setPropertyBoolean(String, boolean...);
     method public BuilderType setPropertyBytes(String, byte[]!...);
@@ -152,25 +238,49 @@
     method public BuilderType setPropertyDouble(String, double...);
     method public BuilderType setPropertyLong(String, long...);
     method public BuilderType setPropertyString(String, java.lang.String!...);
+    method public BuilderType setSchemaType(String);
     method public BuilderType setScore(@IntRange(from=0, to=java.lang.Integer.MAX_VALUE) int);
     method public BuilderType setTtlMillis(long);
   }
 
-  public final class GetByUriRequest {
+  public final class GetByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
   }
 
-  public static final class GetByUriRequest.Builder {
-    ctor public GetByUriRequest.Builder();
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.GetByUriRequest build();
-    method public androidx.appsearch.app.GetByUriRequest.Builder setNamespace(String);
+  public static final class GetByDocumentIdRequest.Builder {
+    ctor public GetByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addProjection(String, java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest build();
   }
 
-  public interface GlobalSearchSession {
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
+  public final class GetSchemaResponse {
+    method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
+    method @IntRange(from=0) public int getVersion();
+  }
+
+  public static final class GetSchemaResponse.Builder {
+    ctor public GetSchemaResponse.Builder();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+    method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
+  }
+
+  public interface GlobalSearchSession extends java.io.Closeable {
+    method public void close();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
+  public abstract class Migrator {
+    ctor public Migrator();
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onDowngrade(int, int, androidx.appsearch.app.GenericDocument);
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onUpgrade(int, int, androidx.appsearch.app.GenericDocument);
+    method public abstract boolean shouldMigrate(int, int);
   }
 
   public class PackageIdentifier {
@@ -180,47 +290,92 @@
   }
 
   public final class PutDocumentsRequest {
-    method public java.util.List<androidx.appsearch.app.GenericDocument!> getDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
     ctor public PutDocumentsRequest.Builder();
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(androidx.appsearch.app.GenericDocument!...);
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
-  public final class RemoveByUriRequest {
+  public final class RemoveByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
   }
 
-  public static final class RemoveByUriRequest.Builder {
-    ctor public RemoveByUriRequest.Builder();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.RemoveByUriRequest build();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder setNamespace(String);
+  public static final class RemoveByDocumentIdRequest.Builder {
+    ctor public RemoveByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest build();
+  }
+
+  public final class ReportSystemUsageRequest {
+    method public String getDatabaseName();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getPackageName();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportSystemUsageRequest.Builder {
+    ctor public ReportSystemUsageRequest.Builder(String, String, String, String);
+    method public androidx.appsearch.app.ReportSystemUsageRequest build();
+    method public androidx.appsearch.app.ReportSystemUsageRequest.Builder setUsageTimestampMillis(long);
+  }
+
+  public final class ReportUsageRequest {
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportUsageRequest.Builder {
+    ctor public ReportUsageRequest.Builder(String, String);
+    method public androidx.appsearch.app.ReportUsageRequest build();
+    method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
   public final class SearchResult {
-    method public androidx.appsearch.app.GenericDocument getDocument();
-    method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatches();
+    method public String getDatabaseName();
+    method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.GenericDocument getGenericDocument();
+    method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
     method public String getPackageName();
+    method public double getRankingSignal();
+  }
+
+  public static final class SearchResult.Builder {
+    ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
+    method public androidx.appsearch.app.SearchResult build();
+    method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
   public static final class SearchResult.MatchInfo {
     method public CharSequence getExactMatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchRange();
     method public String getFullText();
     method public String getPropertyPath();
     method public CharSequence getSnippet();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
+  }
+
+  public static final class SearchResult.MatchInfo.Builder {
+    ctor public SearchResult.MatchInfo.Builder(String);
+    method public androidx.appsearch.app.SearchResult.MatchInfo build();
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setExactMatchRange(androidx.appsearch.app.SearchResult.MatchRange);
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setSnippetRange(androidx.appsearch.app.SearchResult.MatchRange);
   }
 
   public static final class SearchResult.MatchRange {
+    ctor public SearchResult.MatchRange(int, int);
     method public int getEnd();
     method public int getStart();
   }
@@ -231,16 +386,21 @@
   }
 
   public final class SearchSpec {
+    method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaxSnippetSize();
-    method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
-    method public java.util.List<java.lang.String!> getSchemaTypes();
+    method public int getResultGroupingLimit();
+    method public int getResultGroupingTypeFlags();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
+    field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
     field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
@@ -248,49 +408,102 @@
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
     field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; // 0x6
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
+    field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
+    field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
 
   public static final class SearchSpec.Builder {
     ctor public SearchSpec.Builder();
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
+    method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
   }
 
   public final class SetSchemaRequest {
+    method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
-    method public java.util.Set<java.lang.String!> getSchemasNotVisibleToSystemUi();
+    method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
     method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
+    method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(androidx.appsearch.app.AppSearchSchema!...);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
     method public androidx.appsearch.app.SetSchemaRequest build();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForSystemUi(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForSystemUi(String, boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
+  }
+
+  public class SetSchemaResponse {
+    method public java.util.Set<java.lang.String!> getDeletedTypes();
+    method public java.util.Set<java.lang.String!> getIncompatibleTypes();
+    method public java.util.Set<java.lang.String!> getMigratedTypes();
+    method public java.util.List<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!> getMigrationFailures();
+  }
+
+  public static final class SetSchemaResponse.Builder {
+    ctor public SetSchemaResponse.Builder();
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailure(androidx.appsearch.app.SetSchemaResponse.MigrationFailure);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailures(java.util.Collection<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!>);
+    method public androidx.appsearch.app.SetSchemaResponse build();
+  }
+
+  public static class SetSchemaResponse.MigrationFailure {
+    ctor public SetSchemaResponse.MigrationFailure(String, String, String, androidx.appsearch.app.AppSearchResult<?>);
+    method public androidx.appsearch.app.AppSearchResult<java.lang.Void!> getAppSearchResult();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getSchemaType();
+  }
+
+  public class StorageInfo {
+    method public int getAliveDocumentsCount();
+    method public int getAliveNamespacesCount();
+    method public long getSizeBytes();
+  }
+
+  public static final class StorageInfo.Builder {
+    ctor public StorageInfo.Builder();
+    method public androidx.appsearch.app.StorageInfo build();
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
   }
 
 }
@@ -298,6 +511,9 @@
 package androidx.appsearch.exceptions {
 
   public class AppSearchException extends java.lang.Exception {
+    ctor public AppSearchException(int);
+    ctor public AppSearchException(int, String?);
+    ctor public AppSearchException(int, String?, Throwable?);
     method public int getResultCode();
     method public <T> androidx.appsearch.app.AppSearchResult<T!> toAppSearchResult();
   }
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 5760570..8b666c0 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -28,16 +28,21 @@
         sourceCompatibility = JavaVersion.VERSION_1_8
         targetCompatibility = JavaVersion.VERSION_1_8
     }
+    buildTypes.all {
+        consumerProguardFiles "proguard-rules.pro"
+    }
 }
 
 dependencies {
     api('androidx.annotation:annotation:1.1.0')
+    api(libs.jsr250)
 
     implementation('androidx.concurrent:concurrent-futures:1.0.0')
     implementation('androidx.core:core:1.2.0')
 
     androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
     androidTestImplementation project(':appsearch:appsearch-local-storage')
+    androidTestImplementation project(':appsearch:appsearch-platform-storage')
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.truth)
diff --git a/appsearch/appsearch/proguard-rules.pro b/appsearch/appsearch/proguard-rules.pro
new file mode 100644
index 0000000..a7af3cc
--- /dev/null
+++ b/appsearch/appsearch/proguard-rules.pro
@@ -0,0 +1,16 @@
+#  Copyright (C) 2021 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.
+-keep class ** implements androidx.appsearch.app.DocumentClassFactory { *; }
+
+-keep @androidx.appsearch.annotation.Document class *
diff --git a/appsearch/appsearch/src/androidTest/AndroidManifest.xml b/appsearch/appsearch/src/androidTest/AndroidManifest.xml
index 563502a..c2e7297 100644
--- a/appsearch/appsearch/src/androidTest/AndroidManifest.xml
+++ b/appsearch/appsearch/src/androidTest/AndroidManifest.xml
@@ -16,4 +16,6 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="androidx.appsearch.test">
+    <!-- Required for junit TemporaryFolder rule on older API levels -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 </manifest>
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
index 808d90e..b22f8d8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
@@ -29,6 +29,6 @@
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
new file mode 100644
index 0000000..5dbb8a2
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class AnnotationProcessorPlatformTest extends AnnotationProcessorTestBase {
+    @Override
+    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+        Context context = ApplicationProvider.getApplicationContext();
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName).build());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
index 58b80b0..012565b 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -16,16 +16,16 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
 import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.annotation.NonNull;
-import androidx.appsearch.annotation.AppSearchDocument;
-import androidx.appsearch.localstorage.LocalStorage;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.util.AppSearchEmail;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -39,7 +39,7 @@
 
 public abstract class AnnotationProcessorTestBase {
     private AppSearchSession mSession;
-    private static final String DB_NAME_1 = LocalStorage.DEFAULT_DATABASE_NAME;
+    private static final String DB_NAME_1 = "";
 
     protected abstract ListenableFuture<AppSearchSession> createSearchSession(
             @NonNull String dbName);
@@ -63,11 +63,18 @@
         mSession.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
-    @AppSearchDocument
+    @Document
     static class Card {
-        @AppSearchDocument.Uri
-        String mUri;
-        @AppSearchDocument.Property
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.CreationTimestampMillis
+        long mCreationTimestampMillis;
+
+        @Document.StringProperty
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;        // 3a
 
@@ -80,90 +87,96 @@
                 return false;
             }
             Card otherCard = (Card) other;
-            assertThat(otherCard.mUri).isEqualTo(this.mUri);
+            assertThat(otherCard.mId).isEqualTo(this.mId);
             return true;
         }
     }
 
-    @AppSearchDocument
+    @Document
     static class Gift {
-        @AppSearchDocument.Uri
-        String mUri;
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.CreationTimestampMillis
+        long mCreationTimestampMillis;
 
         // Collections
-        @AppSearchDocument.Property
+        @Document.LongProperty
         Collection<Long> mCollectLong;         // 1a
-        @AppSearchDocument.Property
+        @Document.LongProperty
         Collection<Integer> mCollectInteger;   // 1a
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         Collection<Double> mCollectDouble;     // 1a
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         Collection<Float> mCollectFloat;       // 1a
-        @AppSearchDocument.Property
+        @Document.BooleanProperty
         Collection<Boolean> mCollectBoolean;   // 1a
-        @AppSearchDocument.Property
+        @Document.BytesProperty
         Collection<byte[]> mCollectByteArr;    // 1a
-        @AppSearchDocument.Property
+        @Document.StringProperty
         Collection<String> mCollectString;     // 1b
-        @AppSearchDocument.Property
+        @Document.DocumentProperty
         Collection<Card> mCollectCard;         // 1c
 
         // Arrays
-        @AppSearchDocument.Property
+        @Document.LongProperty
         Long[] mArrBoxLong;         // 2a
-        @AppSearchDocument.Property
+        @Document.LongProperty
         long[] mArrUnboxLong;       // 2b
-        @AppSearchDocument.Property
+        @Document.LongProperty
         Integer[] mArrBoxInteger;   // 2a
-        @AppSearchDocument.Property
+        @Document.LongProperty
         int[] mArrUnboxInt;         // 2a
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         Double[] mArrBoxDouble;     // 2a
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         double[] mArrUnboxDouble;   // 2b
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         Float[] mArrBoxFloat;       // 2a
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         float[] mArrUnboxFloat;     // 2a
-        @AppSearchDocument.Property
+        @Document.BooleanProperty
         Boolean[] mArrBoxBoolean;   // 2a
-        @AppSearchDocument.Property
+        @Document.BooleanProperty
         boolean[] mArrUnboxBoolean; // 2b
-        @AppSearchDocument.Property
+        @Document.BytesProperty
         byte[][] mArrUnboxByteArr;  // 2b
-        @AppSearchDocument.Property
+        @Document.BytesProperty
         Byte[] mBoxByteArr;         // 2a
-        @AppSearchDocument.Property
+        @Document.StringProperty
         String[] mArrString;        // 2b
-        @AppSearchDocument.Property
+        @Document.DocumentProperty
         Card[] mArrCard;            // 2c
 
         // Single values
-        @AppSearchDocument.Property
+        @Document.StringProperty
         String mString;        // 3a
-        @AppSearchDocument.Property
+        @Document.LongProperty
         Long mBoxLong;         // 3a
-        @AppSearchDocument.Property
+        @Document.LongProperty
         long mUnboxLong;       // 3b
-        @AppSearchDocument.Property
+        @Document.LongProperty
         Integer mBoxInteger;   // 3a
-        @AppSearchDocument.Property
+        @Document.LongProperty
         int mUnboxInt;         // 3b
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         Double mBoxDouble;     // 3a
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         double mUnboxDouble;   // 3b
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         Float mBoxFloat;       // 3a
-        @AppSearchDocument.Property
+        @Document.DoubleProperty
         float mUnboxFloat;     // 3b
-        @AppSearchDocument.Property
+        @Document.BooleanProperty
         Boolean mBoxBoolean;   // 3a
-        @AppSearchDocument.Property
+        @Document.BooleanProperty
         boolean mUnboxBoolean; // 3b
-        @AppSearchDocument.Property
+        @Document.BytesProperty
         byte[] mUnboxByteArr;  // 3a
-        @AppSearchDocument.Property
+        @Document.DocumentProperty
         Card mCard;            // 3c
 
         @Override
@@ -175,7 +188,8 @@
                 return false;
             }
             Gift otherGift = (Gift) other;
-            assertThat(otherGift.mUri).isEqualTo(this.mUri);
+            assertThat(otherGift.mNamespace).isEqualTo(this.mNamespace);
+            assertThat(otherGift.mId).isEqualTo(this.mId);
             assertThat(otherGift.mArrBoxBoolean).isEqualTo(this.mArrBoxBoolean);
             assertThat(otherGift.mArrBoxDouble).isEqualTo(this.mArrBoxDouble);
             assertThat(otherGift.mArrBoxFloat).isEqualTo(this.mArrBoxFloat);
@@ -224,132 +238,152 @@
             assertThat(second).isNotNull();
             assertThat(first.toArray()).isEqualTo(second.toArray());
         }
+
+        public static Gift createPopulatedGift() {
+            Gift gift = new Gift();
+            gift.mNamespace = "gift.namespace";
+            gift.mId = "gift.id";
+
+            gift.mArrBoxBoolean = new Boolean[]{true, false};
+            gift.mArrBoxDouble = new Double[]{0.0, 1.0};
+            gift.mArrBoxFloat = new Float[]{2.0F, 3.0F};
+            gift.mArrBoxInteger = new Integer[]{4, 5};
+            gift.mArrBoxLong = new Long[]{6L, 7L};
+            gift.mArrString = new String[]{"cat", "dog"};
+            gift.mBoxByteArr = new Byte[]{8, 9};
+            gift.mArrUnboxBoolean = new boolean[]{false, true};
+            gift.mArrUnboxByteArr = new byte[][]{{0, 1}, {2, 3}};
+            gift.mArrUnboxDouble = new double[]{1.0, 0.0};
+            gift.mArrUnboxFloat = new float[]{3.0f, 2.0f};
+            gift.mArrUnboxInt = new int[]{5, 4};
+            gift.mArrUnboxLong = new long[]{7, 6};
+
+            Card card1 = new Card();
+            card1.mNamespace = "card.namespace";
+            card1.mId = "card.id1";
+            Card card2 = new Card();
+            card2.mNamespace = "card.namespace";
+            card2.mId = "card.id2";
+            gift.mArrCard = new Card[]{card2, card2};
+
+            gift.mCollectLong = Arrays.asList(gift.mArrBoxLong);
+            gift.mCollectInteger = Arrays.asList(gift.mArrBoxInteger);
+            gift.mCollectBoolean = Arrays.asList(gift.mArrBoxBoolean);
+            gift.mCollectString = Arrays.asList(gift.mArrString);
+            gift.mCollectDouble = Arrays.asList(gift.mArrBoxDouble);
+            gift.mCollectFloat = Arrays.asList(gift.mArrBoxFloat);
+            gift.mCollectByteArr = Arrays.asList(gift.mArrUnboxByteArr);
+            gift.mCollectCard = Arrays.asList(card2, card2);
+
+            gift.mString = "String";
+            gift.mBoxLong = 1L;
+            gift.mUnboxLong = 2L;
+            gift.mBoxInteger = 3;
+            gift.mUnboxInt = 4;
+            gift.mBoxDouble = 5.0;
+            gift.mUnboxDouble = 6.0;
+            gift.mBoxFloat = 7.0F;
+            gift.mUnboxFloat = 8.0f;
+            gift.mBoxBoolean = true;
+            gift.mUnboxBoolean = false;
+            gift.mUnboxByteArr = new byte[]{1, 2, 3};
+            gift.mCard = card1;
+
+            return gift;
+        }
     }
 
     @Test
     public void testAnnotationProcessor() throws Exception {
         //TODO(b/156296904) add test for int, float, GenericDocument, and class with
-        // @AppSearchDocument annotation
+        // @Document annotation
         mSession.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(Card.class, Gift.class).build()).get();
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class, Gift.class).build())
+                .get();
 
         // Create a Gift object and assign values.
-        Gift inputDataClass = new Gift();
-        inputDataClass.mUri = "gift.uri";
-
-        inputDataClass.mArrBoxBoolean = new Boolean[]{true, false};
-        inputDataClass.mArrBoxDouble = new Double[]{0.0, 1.0};
-        inputDataClass.mArrBoxFloat = new Float[]{2.0F, 3.0F};
-        inputDataClass.mArrBoxInteger = new Integer[]{4, 5};
-        inputDataClass.mArrBoxLong = new Long[]{6L, 7L};
-        inputDataClass.mArrString = new String[]{"cat", "dog"};
-        inputDataClass.mBoxByteArr = new Byte[]{8, 9};
-        inputDataClass.mArrUnboxBoolean = new boolean[]{false, true};
-        inputDataClass.mArrUnboxByteArr = new byte[][]{{0, 1}, {2, 3}};
-        inputDataClass.mArrUnboxDouble = new double[]{1.0, 0.0};
-        inputDataClass.mArrUnboxFloat = new float[]{3.0f, 2.0f};
-        inputDataClass.mArrUnboxInt = new int[]{5, 4};
-        inputDataClass.mArrUnboxLong = new long[]{7, 6};
-
-        Card card1 = new Card();
-        card1.mUri = "card.uri1";
-        Card card2 = new Card();
-        card2.mUri = "card.uri2";
-        inputDataClass.mArrCard = new Card[]{card2, card2};
-
-        inputDataClass.mCollectLong = Arrays.asList(inputDataClass.mArrBoxLong);
-        inputDataClass.mCollectInteger = Arrays.asList(inputDataClass.mArrBoxInteger);
-        inputDataClass.mCollectBoolean = Arrays.asList(inputDataClass.mArrBoxBoolean);
-        inputDataClass.mCollectString = Arrays.asList(inputDataClass.mArrString);
-        inputDataClass.mCollectDouble = Arrays.asList(inputDataClass.mArrBoxDouble);
-        inputDataClass.mCollectFloat = Arrays.asList(inputDataClass.mArrBoxFloat);
-        inputDataClass.mCollectByteArr = Arrays.asList(inputDataClass.mArrUnboxByteArr);
-        inputDataClass.mCollectCard = Arrays.asList(card2, card2);
-
-        inputDataClass.mString = "String";
-        inputDataClass.mBoxLong = 1L;
-        inputDataClass.mUnboxLong = 2L;
-        inputDataClass.mBoxInteger = 3;
-        inputDataClass.mUnboxInt = 4;
-        inputDataClass.mBoxDouble = 5.0;
-        inputDataClass.mUnboxDouble = 6.0;
-        inputDataClass.mBoxFloat = 7.0F;
-        inputDataClass.mUnboxFloat = 8.0f;
-        inputDataClass.mBoxBoolean = true;
-        inputDataClass.mUnboxBoolean = false;
-        inputDataClass.mUnboxByteArr = new byte[]{1, 2, 3};
-        inputDataClass.mCard = card1;
+        Gift inputDocument = Gift.createPopulatedGift();
 
         // Index the Gift document and query it.
-        checkIsBatchResultSuccess(mSession.putDocuments(
-                new PutDocumentsRequest.Builder().addDataClass(inputDataClass).build()));
-        SearchResults searchResults = mSession.query("", new SearchSpec.Builder()
+        checkIsBatchResultSuccess(mSession.put(
+                new PutDocumentsRequest.Builder().addDocuments(inputDocument).build()));
+        SearchResults searchResults = mSession.search("", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(1);
 
-        // Create DataClassFactory for Gift.
-        DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-        DataClassFactory<Gift> factory = registry.getOrCreateFactory(Gift.class);
-
         // Convert GenericDocument to Gift and check values.
-        Gift outputDataClass = factory.fromGenericDocument(documents.get((0)));
-        assertThat(outputDataClass).isEqualTo(inputDataClass);
+        Gift outputDocument = documents.get(0).toDocumentClass(Gift.class);
+        assertThat(outputDocument).isEqualTo(inputDocument);
     }
 
     @Test
     public void testAnnotationProcessor_queryByType() throws Exception {
         mSession.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addDataClass(Card.class, Gift.class)
-                        .addSchema(AppSearchEmail.SCHEMA).build())
+                        .addDocumentClasses(Card.class, Gift.class)
+                        .addSchemas(AppSearchEmail.SCHEMA).build())
                 .get();
 
         // Create documents and index them
-        Gift inputDataClass1 = new Gift();
-        inputDataClass1.mUri = "gift.uri1";
-        Gift inputDataClass2 = new Gift();
-        inputDataClass2.mUri = "gift.uri2";
+        Gift inputDocument1 = new Gift();
+        inputDocument1.mNamespace = "gift.namespace";
+        inputDocument1.mId = "gift.id1";
+        Gift inputDocument2 = new Gift();
+        inputDocument2.mNamespace = "gift.namespace";
+        inputDocument2.mId = "gift.id2";
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri3")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id3")
                         .setFrom("from@example.com")
                         .setTo("to1@example.com", "to2@example.com")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mSession.putDocuments(
+        checkIsBatchResultSuccess(mSession.put(
                 new PutDocumentsRequest.Builder()
-                        .addDataClass(inputDataClass1, inputDataClass2)
-                        .addGenericDocument(email1).build()));
+                        .addDocuments(inputDocument1, inputDocument2)
+                        .addGenericDocuments(email1).build()));
 
         // Query the documents by it's schema type.
-        SearchResults searchResults = mSession.query("",
+        SearchResults searchResults = mSession.search("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaType("Gift", AppSearchEmail.SCHEMA_TYPE)
+                        .addFilterSchemas("Gift", AppSearchEmail.SCHEMA_TYPE)
                         .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(3);
 
         // Query the documents by it's class.
-        searchResults = mSession.query("",
+        searchResults = mSession.search("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaByDataClass(Gift.class)
+                        .addFilterDocumentClasses(Gift.class)
                         .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(2);
 
         // Query the documents by schema type and class mix.
-        searchResults = mSession.query("",
+        searchResults = mSession.search("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
-                        .addSchemaByDataClass(Gift.class)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                        .addFilterDocumentClasses(Gift.class)
                         .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(3);
     }
+
+    @Test
+    public void testGenericDocumentConversion() throws Exception {
+        Gift inGift = Gift.createPopulatedGift();
+        GenericDocument genericDocument1 = GenericDocument.fromDocumentClass(inGift);
+        GenericDocument genericDocument2 = GenericDocument.fromDocumentClass(inGift);
+        Gift outGift = genericDocument2.toDocumentClass(Gift.class);
+
+        assertThat(inGift).isNotSameInstanceAs(outGift);
+        assertThat(inGift).isEqualTo(outGift);
+        assertThat(genericDocument1).isNotSameInstanceAs(genericDocument2);
+        assertThat(genericDocument1).isEqualTo(genericDocument2);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java
index ee68c88..ef88e46 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java
@@ -18,13 +18,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.appsearch.app.util.AppSearchEmail;
+
 import org.junit.Test;
 
 public class AppSearchEmailTest {
 
     @Test
     public void testBuildEmailAndGetValue() {
-        AppSearchEmail email = new AppSearchEmail.Builder("uri")
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id")
                 .setFrom("FakeFromAddress")
                 .setCc("CC1", "CC2")
                 // Score and Property are mixed into the middle to make sure DocumentBuilder's
@@ -35,7 +37,8 @@
                 .setBody("EmailBody")
                 .build();
 
-        assertThat(email.getUri()).isEqualTo("uri");
+        assertThat(email.getNamespace()).isEqualTo("namespace");
+        assertThat(email.getId()).isEqualTo("id");
         assertThat(email.getFrom()).isEqualTo("FakeFromAddress");
         assertThat(email.getTo()).isNull();
         assertThat(email.getCc()).asList().containsExactly("CC1", "CC2");
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
new file mode 100644
index 0000000..125e55f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+public class AppSearchResultTest {
+    @Test
+    public void testMapNullPointerException() {
+        NullPointerException e = assertThrows(NullPointerException.class, () -> {
+            Object o = null;
+            o.toString();
+        });
+        AppSearchResult<?> result = AppSearchResult.throwableToFailedResult(e);
+        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_INTERNAL_ERROR);
+        // Makes sure the exception name is included in the string. Some exceptions have terse or
+        // missing strings so it's confusing to read the output without the exception name.
+        assertThat(result.getErrorMessage()).startsWith("NullPointerException");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
new file mode 100644
index 0000000..c15f290
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+
+public class GenericDocumentTest {
+    @Test
+    public void testRecreateFromParcel() {
+        GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Hello")
+                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+                .setPropertyDocument(
+                        "propDocument",
+                        new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                                .setPropertyString("propString", "Goodbye")
+                                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                                .build())
+                .build();
+
+        // Serialize the document
+        Parcel inParcel = Parcel.obtain();
+        inParcel.writeBundle(inDoc.getBundle());
+        byte[] data = inParcel.marshall();
+        inParcel.recycle();
+
+        // Deserialize the document
+        Parcel outParcel = Parcel.obtain();
+        outParcel.unmarshall(data, 0, data.length);
+        outParcel.setDataPosition(0);
+        Bundle outBundle = outParcel.readBundle();
+        outParcel.recycle();
+
+        // Compare results
+        GenericDocument outDoc = new GenericDocument(outBundle);
+        assertThat(inDoc).isEqualTo(outDoc);
+        assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
+        assertThat(outDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+        assertThat(outDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+                .isEqualTo("Goodbye");
+        assertThat(outDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+                .isEqualTo(new byte[][]{{3, 4}});
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/PutDocumentsRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/PutDocumentsRequestTest.java
deleted file mode 100644
index bd61231..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/PutDocumentsRequestTest.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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 androidx.appsearch.app;
-
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-
-import androidx.appsearch.annotation.AppSearchDocument;
-import androidx.appsearch.localstorage.LocalStorage;
-import androidx.test.core.app.ApplicationProvider;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Test;
-
-import java.util.Set;
-
-public class PutDocumentsRequestTest {
-
-    @Test
-    public void addGenericDocument_byCollection() {
-        Set<AppSearchEmail> emails = ImmutableSet.of(new AppSearchEmail.Builder("test1").build(),
-                new AppSearchEmail.Builder("test2").build());
-        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addGenericDocument(emails)
-                .build();
-
-        assertThat(request.getDocuments().get(0).getUri()).isEqualTo("test1");
-        assertThat(request.getDocuments().get(1).getUri()).isEqualTo("test2");
-    }
-
-// @exportToFramework:startStrip()
-    @AppSearchDocument
-    static class Card {
-        @AppSearchDocument.Uri
-        String mUri;
-
-        @AppSearchDocument.Property(indexingType = INDEXING_TYPE_PREFIXES)
-        String mString;
-
-        Card(String mUri, String mString) {
-            this.mUri = mUri;
-            this.mString = mString;
-        }
-    }
-
-    @Test
-    public void addDataClass_byCollection() throws Exception {
-        // A schema with Card must be set in order to be able to add a Card instance to
-        // PutDocumentsRequest.
-        Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession session = LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context)
-                        .setDatabaseName(LocalStorage.DEFAULT_DATABASE_NAME)
-                        .build()
-        ).get();
-        session.setSchema(new SetSchemaRequest.Builder().addDataClass(Card.class).build()).get();
-
-        Set<Card> cards = ImmutableSet.of(new Card("cardUri", "cardProperty"));
-        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDataClass(cards)
-                .build();
-
-        assertThat(request.getDocuments().get(0).getUri()).isEqualTo("cardUri");
-    }
-// @exportToFramework:endStrip()
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
index 5d98ead..19ccc63 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
@@ -16,31 +16,21 @@
 
 package androidx.appsearch.app;
 
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.Bundle;
 
-import androidx.appsearch.annotation.AppSearchDocument;
-
-import com.google.common.collect.ImmutableSet;
-
 import org.junit.Test;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
 public class SearchSpecTest {
 
     @Test
     public void testGetBundle() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addNamespace("namespace1", "namespace2")
-                .addSchemaType("schemaTypes1", "schemaTypes2")
+                .addFilterNamespaces("namespace1", "namespace2")
+                .addFilterSchemas("schemaTypes1", "schemaTypes2")
+                .addFilterPackageNames("package1", "package2")
                 .setSnippetCount(5)
                 .setSnippetCountPerProperty(10)
                 .setMaxSnippetSize(15)
@@ -54,8 +44,10 @@
                 .isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
         assertThat(bundle.getStringArrayList(SearchSpec.NAMESPACE_FIELD)).containsExactly(
                 "namespace1", "namespace2");
-        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_TYPE_FIELD)).containsExactly(
+        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_FIELD)).containsExactly(
                 "schemaTypes1", "schemaTypes2");
+        assertThat(bundle.getStringArrayList(SearchSpec.PACKAGE_NAME_FIELD)).containsExactly(
+                "package1", "package2");
         assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_FIELD)).isEqualTo(5);
         assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_PER_PROPERTY_FIELD)).isEqualTo(10);
         assertThat(bundle.getInt(SearchSpec.MAX_SNIPPET_FIELD)).isEqualTo(15);
@@ -64,58 +56,4 @@
         assertThat(bundle.getInt(SearchSpec.RANKING_STRATEGY_FIELD))
                 .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE);
     }
-
-    @Test
-    public void testGetProjectionTypePropertyMasks() {
-        SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addProjection("TypeA", "field1", "field2.subfield2")
-                .addProjection("TypeB", "field7")
-                .addProjection("TypeC")
-                .build();
-
-        Map<String, List<String>> typePropertyPathMap = searchSpec.getProjections();
-        assertThat(typePropertyPathMap.keySet())
-                .containsExactly("TypeA", "TypeB", "TypeC");
-        assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
-        assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
-        assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
-    }
-
-    @Test
-    public void testGetRankingStrategy() {
-        SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
-                .build();
-        assertThat(searchSpec.getRankingStrategy()).isEqualTo(
-                SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE);
-    }
-
-// @exportToFramework:startStrip()
-    @AppSearchDocument
-    static class King extends Card {
-        @AppSearchDocument.Uri
-        String mUri;
-
-        @AppSearchDocument.Property
-                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
-        String mString;
-    }
-
-    static class Card {}
-
-    @Test
-    public void testAddSchemaByDataClass_byCollection() throws Exception {
-        Set<Class<King>> cardClassSet = ImmutableSet.of(King.class);
-        SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addSchemaByDataClass(cardClassSet)
-                .build();
-
-        Bundle bundle = searchSpec.getBundle();
-        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_TYPE_FIELD)).containsExactly(
-                "King");
-    }
-// @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
deleted file mode 100644
index 4b8b21d..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
+++ /dev/null
@@ -1,337 +0,0 @@
-/*
- * 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 androidx.appsearch.app;
-
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import androidx.appsearch.annotation.AppSearchDocument;
-import androidx.collection.ArrayMap;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Test;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-public class SetSchemaRequestTest {
-// @exportToFramework:startStrip()
-    @AppSearchDocument
-    static class Card {
-        @AppSearchDocument.Uri
-        String mUri;
-
-        @AppSearchDocument.Property
-                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
-        String mString;
-
-        @Override
-        public boolean equals(Object other) {
-            if (this == other) {
-                return true;
-            }
-            if (!(other instanceof AnnotationProcessorTestBase.Card)) {
-                return false;
-            }
-            AnnotationProcessorTestBase.Card otherCard = (AnnotationProcessorTestBase.Card) other;
-            assertThat(otherCard.mUri).isEqualTo(this.mUri);
-            return true;
-        }
-    }
-
-    static class Spade {}
-
-    @AppSearchDocument
-    static class King extends Spade {
-        @AppSearchDocument.Uri
-        String mUri;
-
-        @AppSearchDocument.Property
-                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
-        String mString;
-    }
-
-    @AppSearchDocument
-    static class Queen extends Spade {
-        @AppSearchDocument.Uri
-        String mUri;
-
-        @AppSearchDocument.Property
-                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
-        String mString;
-    }
-// @exportToFramework:endStrip()
-
-    private static Collection<String> getSchemaTypesFromSetSchemaRequest(SetSchemaRequest request) {
-        HashSet<String> schemaTypes = new HashSet<>();
-        for (AppSearchSchema schema : request.getSchemas()) {
-            schemaTypes.add(schema.getSchemaType());
-        }
-        return schemaTypes;
-    }
-
-    @Test
-    public void testInvalidSchemaReferences_fromSystemUiVisibility() {
-        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
-                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(
-                        "InvalidSchema", false).build());
-        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
-    }
-
-    @Test
-    public void testInvalidSchemaReferences_fromPackageVisibility() {
-        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
-                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForPackage(
-                        "InvalidSchema", /*visible=*/ true, new PackageIdentifier(
-                                "com.foo.package", /*sha256Certificate=*/ new byte[]{})).build());
-        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
-    }
-
-    @Test
-    public void testSchemaTypeVisibilityForSystemUi_visible() {
-        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
-
-        // By default, the schema is visible.
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
-
-        request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
-                        "Schema", true).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
-    }
-
-    @Test
-    public void testSchemaTypeVisibilityForSystemUi_notVisible() {
-        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
-                        "Schema", false).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Schema");
-    }
-
-// @exportToFramework:startStrip()
-    @Test
-    public void testDataClassVisibilityForSystemUi_visible() throws Exception {
-        // By default, the schema is visible.
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
-
-        request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForSystemUi(
-                        Card.class, true).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
-    }
-
-    @Test
-    public void testDataClassVisibilityForSystemUi_notVisible() throws Exception {
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForSystemUi(
-                        Card.class, false).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Card");
-    }
-// @exportToFramework:endStrip()
-
-    @Test
-    public void testSchemaTypeVisibilityForPackage_visible() {
-        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
-
-        // By default, the schema is not visible.
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).build();
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-
-        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
-                new byte[]{100});
-        Map<String, Set<PackageIdentifier>> expectedVisibleToPackagesMap = new ArrayMap<>();
-        expectedVisibleToPackagesMap.put("Schema", Collections.singleton(packageIdentifier));
-
-        request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
-                        "Schema", /*visible=*/ true, packageIdentifier).build();
-        assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
-                expectedVisibleToPackagesMap);
-    }
-
-    @Test
-    public void testSchemaTypeVisibilityForPackage_notVisible() {
-        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
-
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
-                        "Schema", /*visible=*/ false, new PackageIdentifier("com.package.foo",
-                                /*sha256Certificate=*/ new byte[]{})).build();
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-    }
-
-    @Test
-    public void testSchemaTypeVisibilityForPackage_deduped() throws Exception {
-        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
-
-        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
-                new byte[]{100});
-        Map<String, Set<PackageIdentifier>> expectedVisibleToPackagesMap = new ArrayMap<>();
-        expectedVisibleToPackagesMap.put("Schema", Collections.singleton(packageIdentifier));
-
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder()
-                        .addSchema(schema)
-                        // Set it visible for "Schema"
-                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
-                                true, packageIdentifier)
-                        // Set it visible for "Schema" again, which should be a no-op
-                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
-                                true, packageIdentifier)
-                        .build();
-        assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
-                expectedVisibleToPackagesMap);
-    }
-
-    @Test
-    public void testSchemaTypeVisibilityForPackage_removed() throws Exception {
-        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
-
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder()
-                        .addSchema(schema)
-                        // First set it as visible
-                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
-                                true, new PackageIdentifier("com.package.foo",
-                                        /*sha256Certificate=*/ new byte[]{100}))
-                        // Then make it not visible
-                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
-                                false, new PackageIdentifier("com.package.foo",
-                                        /*sha256Certificate=*/ new byte[]{100}))
-                        .build();
-
-        // Nothing should be visible.
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-    }
-
-// @exportToFramework:startStrip()
-    @Test
-    public void testDataClassVisibilityForPackage_visible() throws Exception {
-        // By default, the schema is not visible.
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-
-        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
-                new byte[]{100});
-        Map<String, Set<PackageIdentifier>> expectedVisibleToPackagesMap = new ArrayMap<>();
-        expectedVisibleToPackagesMap.put("Card", Collections.singleton(packageIdentifier));
-
-        request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForPackage(
-                        Card.class, /*visible=*/ true, packageIdentifier).build();
-        assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
-                expectedVisibleToPackagesMap);
-    }
-
-    @Test
-    public void testDataClassVisibilityForPackage_notVisible() throws Exception {
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForPackage(
-                        Card.class, /*visible=*/ false,
-                        new PackageIdentifier("com.package.foo", /*sha256Certificate=*/
-                                new byte[]{})).build();
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-    }
-
-    @Test
-    public void testDataClassVisibilityForPackage_deduped() throws Exception {
-        // By default, the schema is not visible.
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-
-        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
-                new byte[]{100});
-        Map<String, Set<PackageIdentifier>> expectedVisibleToPackagesMap = new ArrayMap<>();
-        expectedVisibleToPackagesMap.put("Card", Collections.singleton(packageIdentifier));
-
-        request =
-                new SetSchemaRequest.Builder()
-                        .addDataClass(Card.class)
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
-                                true, packageIdentifier)
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
-                                true, packageIdentifier)
-                        .build();
-        assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
-                expectedVisibleToPackagesMap);
-    }
-
-    @Test
-    public void testDataClassVisibilityForPackage_removed() throws Exception {
-        // By default, the schema is not visible.
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-
-        request =
-                new SetSchemaRequest.Builder()
-                        .addDataClass(Card.class)
-                        // First set it as visible
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
-                                true, new PackageIdentifier("com.package.foo",
-                                        /*sha256Certificate=*/ new byte[]{100}))
-                        // Then make it not visible
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
-                                false, new PackageIdentifier("com.package.foo",
-                                        /*sha256Certificate=*/ new byte[]{100}))
-                        .build();
-
-        // Nothing should be visible.
-        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
-    }
-
-    @Test
-    public void testAddDataClass_byCollection() throws Exception {
-        Set<Class<? extends Spade>> cardClasses = ImmutableSet.of(Queen.class, King.class);
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(cardClasses)
-                        .build();
-        assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
-                "King");
-    }
-
-    @Test
-    public void testAddDataClass_byCollectionWithSeparateCalls() throws
-            Exception {
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(ImmutableSet.of(Queen.class))
-                        .addDataClass(ImmutableSet.of(King.class)).build();
-        assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
-                "King");
-    }
-// @exportToFramework:endStrip()
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java
new file mode 100644
index 0000000..8aa6148
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class SetSchemaResponseTest {
+    @Test
+    public void testRebuild() {
+        SetSchemaResponse.MigrationFailure failure1 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure1",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"));
+        SetSchemaResponse.MigrationFailure failure2 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure2",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR,  "errorMessage"));
+
+        SetSchemaResponse original = new SetSchemaResponse.Builder()
+                .addDeletedType("delete1")
+                .addIncompatibleType("incompatible1")
+                .addMigratedType("migrated1")
+                .addMigrationFailure(failure1)
+                .build();
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        SetSchemaResponse rebuild = original.toBuilder()
+                        .addDeletedType("delete2")
+                        .addIncompatibleType("incompatible2")
+                        .addMigratedType("migrated2")
+                        .addMigrationFailure(failure2)
+                        .build();
+
+        // rebuild won't effect the original object
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        assertThat(rebuild.getDeletedTypes()).containsExactly("delete1", "delete2");
+        assertThat(rebuild.getIncompatibleTypes()).containsExactly("incompatible1",
+                "incompatible2");
+        assertThat(rebuild.getMigratedTypes()).containsExactly("migrated1", "migrated2");
+        assertThat(rebuild.getMigrationFailures()).containsExactly(failure1, failure2);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
deleted file mode 100644
index 7a646b0..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * 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 androidx.appsearch.app.cts;
-
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
-import androidx.appsearch.exceptions.IllegalSchemaException;
-
-import org.junit.Test;
-
-public class AppSearchSchemaCtsTest {
-    @Test
-    public void testInvalidEnums() {
-        PropertyConfig.Builder builder = new PropertyConfig.Builder("test");
-        assertThrows(IllegalArgumentException.class, () -> builder.setDataType(99));
-        assertThrows(IllegalArgumentException.class, () -> builder.setCardinality(99));
-    }
-
-    @Test
-    public void testMissingFields() {
-        PropertyConfig.Builder builder = new PropertyConfig.Builder("test");
-        IllegalSchemaException e = assertThrows(IllegalSchemaException.class, builder::build);
-        assertThat(e).hasMessageThat().contains("Missing field: dataType");
-
-        builder.setDataType(PropertyConfig.DATA_TYPE_DOCUMENT);
-        e = assertThrows(IllegalSchemaException.class, builder::build);
-        assertThat(e).hasMessageThat().contains("Missing field: schemaType");
-
-        builder.setSchemaType("TestType");
-        e = assertThrows(IllegalSchemaException.class, builder::build);
-        assertThat(e).hasMessageThat().contains("Missing field: cardinality");
-
-        builder.setCardinality(PropertyConfig.CARDINALITY_REPEATED);
-        builder.build();
-    }
-
-    @Test
-    public void testDuplicateProperties() {
-        AppSearchSchema.Builder builder = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                );
-        IllegalSchemaException e = assertThrows(IllegalSchemaException.class,
-                () -> builder.addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()));
-        assertThat(e).hasMessageThat().contains("Property defined more than once: subject");
-    }
-
-    @Test
-    public void testEquals_identical() {
-        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        assertThat(schema1).isEqualTo(schema2);
-        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
-    }
-
-    @Test
-    public void testEquals_differentOrder() {
-        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build()
-                ).build();
-        assertThat(schema1).isEqualTo(schema2);
-        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
-    }
-
-    @Test
-    public void testEquals_failure() {
-        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        assertThat(schema1).isNotEqualTo(schema2);
-        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
-    }
-
-    @Test
-    public void testEquals_failure_differentOrder() {
-        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        // Order of 'body' and 'subject' has been switched
-        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        assertThat(schema1).isNotEqualTo(schema2);
-        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
deleted file mode 100644
index b23a2a17..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
+++ /dev/null
@@ -1,1851 +0,0 @@
-/*
- * 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 androidx.appsearch.app.cts;
-
-import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
-import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
-import static androidx.appsearch.app.util.AppSearchTestUtils.doGet;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.appsearch.app.AppSearchBatchResult;
-import androidx.appsearch.app.AppSearchEmail;
-import androidx.appsearch.app.AppSearchResult;
-import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
-import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByUriRequest;
-import androidx.appsearch.app.PutDocumentsRequest;
-import androidx.appsearch.app.RemoveByUriRequest;
-import androidx.appsearch.app.SearchResult;
-import androidx.appsearch.app.SearchResults;
-import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.cts.customer.EmailDataClass;
-import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.localstorage.LocalStorage;
-import androidx.test.core.app.ApplicationProvider;
-
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-
-public abstract class AppSearchSessionCtsTestBase {
-    private AppSearchSession mDb1;
-    private static final String DB_NAME_1 = LocalStorage.DEFAULT_DATABASE_NAME;
-    private AppSearchSession mDb2;
-    private static final String DB_NAME_2 = "testDb2";
-
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
-            @NonNull String dbName);
-
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
-            @NonNull String dbName, @NonNull ExecutorService executor);
-
-    @Before
-    public void setUp() throws Exception {
-        Context context = ApplicationProvider.getApplicationContext();
-
-        mDb1 = createSearchSession(DB_NAME_1).get();
-        mDb2 = createSearchSession(DB_NAME_2).get();
-
-        // Cleanup whatever documents may still exist in these databases. This is needed in
-        // addition to tearDown in case a test exited without completing properly.
-        cleanup();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        // Cleanup whatever documents may still exist in these databases.
-        cleanup();
-    }
-
-    private void cleanup() throws Exception {
-        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-    }
-
-    @Test
-    public void testSetSchema() throws Exception {
-        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
-    }
-
-// @exportToFramework:startStrip()
-
-    @Test
-    public void testSetSchema_dataClass() throws Exception {
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
-    }
-// @exportToFramework:endStrip()
-
-// @exportToFramework:startStrip()
-
-    @Test
-    public void testGetSchema() throws Exception {
-        AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-        AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-
-        SetSchemaRequest request1 = new SetSchemaRequest.Builder()
-                .addSchema(emailSchema1).addDataClass(EmailDataClass.class).build();
-        SetSchemaRequest request2 = new SetSchemaRequest.Builder()
-                .addSchema(emailSchema2).addDataClass(EmailDataClass.class).build();
-
-        mDb1.setSchema(request1).get();
-        mDb2.setSchema(request2).get();
-
-        Set<AppSearchSchema> actual1 = mDb1.getSchema().get();
-        Set<AppSearchSchema> actual2 = mDb2.getSchema().get();
-
-        assertThat(actual1).isEqualTo(request1.getSchemas());
-        assertThat(actual2).isEqualTo(request2.getSchemas());
-    }
-// @exportToFramework:endStrip()
-
-    @Test
-    public void testPutDocuments() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index a document
-        AppSearchEmail email = new AppSearchEmail.Builder("uri1")
-                .setFrom("from@example.com")
-                .setTo("to1@example.com", "to2@example.com")
-                .setSubject("testPut example")
-                .setBody("This is the body of the testPut email")
-                .build();
-
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
-        assertThat(result.getSuccesses()).containsExactly("uri1", null);
-        assertThat(result.getFailures()).isEmpty();
-    }
-
-// @exportToFramework:startStrip()
-
-    @Test
-    public void testPutDocuments_dataClass() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
-
-        // Index a document
-        EmailDataClass email = new EmailDataClass();
-        email.uri = "uri1";
-        email.subject = "testPut example";
-        email.body = "This is the body of the testPut email";
-
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addDataClass(email).build()));
-        assertThat(result.getSuccesses()).containsExactly("uri1", null);
-        assertThat(result.getFailures()).isEmpty();
-    }
-// @exportToFramework:endStrip()
-
-    @Test
-    public void testUpdateSchema() throws Exception {
-        // Schema registration
-        AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        AppSearchSchema newEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-                        .setDataType(PropertyConfig.DATA_TYPE_INT64)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_NONE)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_NONE)
-                        .build())
-                .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(oldEmailSchema).build()).get();
-
-        // Try to index a gift. This should fail as it's not in the schema.
-        GenericDocument gift =
-                new GenericDocument.Builder<>("gift1", "Gift").setPropertyLong("price", 5).build();
-        AppSearchBatchResult<String, Void> result =
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(gift).build()).get();
-        assertThat(result.isSuccess()).isFalse();
-        assertThat(result.getFailures().get("gift1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Update the schema to include the gift and update email with a new field
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(newEmailSchema, giftSchema).build()).get();
-
-        // Try to index the document again, which should now work
-        checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(gift).build()));
-
-        // Indexing an email with a body should also work
-        AppSearchEmail email = new AppSearchEmail.Builder("email1")
-                .setSubject("testPut example")
-                .setBody("This is the body of the testPut email")
-                .build();
-        checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
-    }
-
-    @Test
-    public void testRemoveSchema() throws Exception {
-        // Schema registration
-        AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
-
-        // Index an email and check it present.
-        AppSearchEmail email = new AppSearchEmail.Builder("email1")
-                .setSubject("testPut example")
-                .build();
-        checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
-        List<GenericDocument> outDocuments =
-                doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "email1");
-        assertThat(outDocuments).hasSize(1);
-        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
-        assertThat(outEmail).isEqualTo(email);
-
-        // Try to remove the email schema. This should fail as it's an incompatible change.
-        Throwable failResult1 = assertThrows(
-                ExecutionException.class,
-                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
-        assertThat(failResult1).isInstanceOf(AppSearchException.class);
-        assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
-        assertThat(failResult1).hasMessageThat().contains(
-                "Deleted types: [androidx.appsearch.test$" + DB_NAME_1 + "/builtin:Email]");
-
-        // Try to remove the email schema again, which should now work as we set forceOverride to
-        // be true.
-        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-
-        // Make sure the indexed email is gone.
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder()
-                        .setNamespace(GenericDocument.DEFAULT_NAMESPACE)
-                        .addUri("email1")
-                        .build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("email1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Try to index an email again. This should fail as the schema has been removed.
-        AppSearchEmail email2 = new AppSearchEmail.Builder("email2")
-                .setSubject("testPut example")
-                .build();
-        AppSearchBatchResult<String, Void> failResult2 = mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()).get();
-        assertThat(failResult2.isSuccess()).isFalse();
-        assertThat(failResult2.getFailures().get("email2").getErrorMessage())
-                .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
-                        + "/builtin:Email' not found");
-    }
-
-    @Test
-    public void testRemoveSchema_twoDatabases() throws Exception {
-        // Schema registration in mDb1 and mDb2
-        AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
-
-        // Index an email and check it present in database1.
-        AppSearchEmail email1 = new AppSearchEmail.Builder("email1")
-                .setSubject("testPut example")
-                .build();
-        checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-        List<GenericDocument> outDocuments =
-                doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "email1");
-        assertThat(outDocuments).hasSize(1);
-        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
-        assertThat(outEmail).isEqualTo(email1);
-
-        // Index an email and check it present in database2.
-        AppSearchEmail email2 = new AppSearchEmail.Builder("email2")
-                .setSubject("testPut example")
-                .build();
-        checkIsBatchResultSuccess(
-                mDb2.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
-        outDocuments = doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "email2");
-        assertThat(outDocuments).hasSize(1);
-        outEmail = new AppSearchEmail(outDocuments.get(0));
-        assertThat(outEmail).isEqualTo(email2);
-
-        // Try to remove the email schema in database1. This should fail as it's an incompatible
-        // change.
-        Throwable failResult1 = assertThrows(
-                ExecutionException.class,
-                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
-        assertThat(failResult1).isInstanceOf(AppSearchException.class);
-        assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
-        assertThat(failResult1).hasMessageThat().contains(
-                "Deleted types: [androidx.appsearch.test$" + DB_NAME_1 + "/builtin:Email]");
-
-        // Try to remove the email schema again, which should now work as we set forceOverride to
-        // be true.
-        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-
-        // Make sure the indexed email is gone in database 1.
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace(GenericDocument.DEFAULT_NAMESPACE)
-                        .addUri("email1").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("email1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Try to index an email again. This should fail as the schema has been removed.
-        AppSearchEmail email3 = new AppSearchEmail.Builder("email3")
-                .setSubject("testPut example")
-                .build();
-        AppSearchBatchResult<String, Void> failResult2 = mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email3).build()).get();
-        assertThat(failResult2.isSuccess()).isFalse();
-        assertThat(failResult2.getFailures().get("email3").getErrorMessage())
-                .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
-                        + "/builtin:Email' not found");
-
-        // Make sure email in database 2 still present.
-        outDocuments = doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "email2");
-        assertThat(outDocuments).hasSize(1);
-        outEmail = new AppSearchEmail(outDocuments.get(0));
-        assertThat(outEmail).isEqualTo(email2);
-
-        // Make sure email could still be indexed in database 2.
-        checkIsBatchResultSuccess(
-                mDb2.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
-    }
-
-    @Test
-    public void testGetDocuments() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index a document
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
-
-        // Get the document
-        List<GenericDocument> outDocuments = doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1");
-        assertThat(outDocuments).hasSize(1);
-        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
-        assertThat(outEmail).isEqualTo(inEmail);
-
-        // Can't get the document in the other instance.
-        AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(failResult.isSuccess()).isFalse();
-        assertThat(failResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-    }
-
-// @exportToFramework:startStrip()
-
-    @Test
-    public void testGetDocuments_dataClass() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
-
-        // Index a document
-        EmailDataClass inEmail = new EmailDataClass();
-        inEmail.uri = "uri1";
-        inEmail.subject = "testPut example";
-        inEmail.body = "This is the body of the testPut inEmail";
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addDataClass(inEmail).build()));
-
-        // Get the document
-        List<GenericDocument> outDocuments = doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1");
-        assertThat(outDocuments).hasSize(1);
-        EmailDataClass outEmail = outDocuments.get(0).toDataClass(EmailDataClass.class);
-        assertThat(inEmail.uri).isEqualTo(outEmail.uri);
-        assertThat(inEmail.subject).isEqualTo(outEmail.subject);
-        assertThat(inEmail.body).isEqualTo(outEmail.body);
-    }
-// @exportToFramework:endStrip()
-
-    @Test
-    public void testQuery() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index a document
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
-
-        // Query for the document
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents.get(0)).isEqualTo(inEmail);
-
-        // Multi-term query
-        searchResults = mDb1.query("body email", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents.get(0)).isEqualTo(inEmail);
-    }
-
-    @Test
-    public void testQuery_getNextPage() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-        Set<AppSearchEmail> emailSet = new HashSet<>();
-        PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
-        // Index 31 documents
-        for (int i = 0; i < 31; i++) {
-            AppSearchEmail inEmail =
-                    new AppSearchEmail.Builder("uri" + i)
-                            .setFrom("from@example.com")
-                            .setTo("to1@example.com", "to2@example.com")
-                            .setSubject("testPut example")
-                            .setBody("This is the body of the testPut email")
-                            .build();
-            emailSet.add(inEmail);
-            putDocumentsRequestBuilder.addGenericDocument(inEmail);
-        }
-        checkIsBatchResultSuccess(mDb1.putDocuments(putDocumentsRequestBuilder.build()));
-
-        // Set number of results per page is 7.
-        SearchResults searchResults = mDb1.query("body",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultCountPerPage(7)
-                        .build());
-        List<GenericDocument> documents = new ArrayList<>();
-
-        int pageNumber = 0;
-        List<SearchResult> results;
-
-        // keep loading next page until it's empty.
-        do {
-            results = searchResults.getNextPage().get();
-            ++pageNumber;
-            for (SearchResult result : results) {
-                documents.add(result.getDocument());
-            }
-        } while (results.size() > 0);
-
-        // check all document presents
-        assertThat(documents).containsExactlyElementsIn(emailSet);
-        assertThat(pageNumber).isEqualTo(6); // 5 (upper(31/7)) + 1 (final empty page)
-    }
-
-    @Test
-    public void testQuery_relevanceScoring() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("Mary had a little lamb")
-                        .setBody("A little lamb, little lamb")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("I'm a little teapot")
-                        .setBody("short and stout. Here is my handle, here is my spout.")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1, email2).build()));
-
-        // Query for "little". It should match both emails.
-        SearchResults searchResults = mDb1.query("little", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email1 should be ranked higher because 'little' appears three times in email1 and
-        // only once in email2.
-        assertThat(documents).containsExactly(email1, email2).inOrder();
-
-        // Query for "little OR stout". It should match both emails.
-        searchResults = mDb1.query("little OR stout", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
-                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email2 should be ranked higher because 'little' appears once and "stout", which is a
-        // rarer term, appears once. email1 only has the three 'little' appearances.
-        assertThat(documents).containsExactly(email2, email1).inOrder();
-    }
-
-    @Test
-    public void testQuery_typeFilter() throws Exception {
-        // Schema registration
-        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("foo")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .build()
-                ).build();
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(genericSchema)
-                        .build()).get();
-
-        // Index a document
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument inDoc = new GenericDocument.Builder<>("uri2", "Generic")
-                .setPropertyString("foo", "body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail, inDoc).build()));
-
-        // Query for the documents
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-        assertThat(documents).containsExactly(inEmail, inDoc);
-
-        // Query only for Document
-        searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .addSchemaType("Generic", "Generic") // duplicate type in filter won't matter.
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(inDoc);
-    }
-
-    @Test
-    public void testQuery_namespaceFilter() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-            .get();
-
-        // Index two documents
-        AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("expectedNamespace")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail unexpectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("unexpectedNamespace")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(expectedEmail, unexpectedEmail).build()));
-
-        // Query for all namespaces
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-        assertThat(documents).containsExactly(expectedEmail, unexpectedEmail);
-
-        // Query only for expectedNamespace
-        searchResults = mDb1.query("body",
-                new SearchSpec.Builder()
-                        .addNamespace("expectedNamespace")
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(expectedEmail);
-    }
-
-    @Test
-    public void testQuery_getPackageName() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index a document
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
-
-        // Query for the document
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-
-        List<SearchResult> results;
-        List<GenericDocument> documents = new ArrayList<>();
-        // keep loading next page until it's empty.
-        do {
-            results = searchResults.getNextPage().get();
-            for (SearchResult result : results) {
-                assertThat(result.getDocument()).isEqualTo(inEmail);
-                assertThat(result.getPackageName()).isEqualTo(
-                        ApplicationProvider.getApplicationContext().getPackageName());
-                documents.add(result.getDocument());
-            }
-        } while (results.size() > 0);
-        assertThat(documents).hasSize(1);
-    }
-
-    @Test
-    public void testQuery_projection() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
-
-        // Query with type property paths {"Email", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(AppSearchEmail.SCHEMA_TYPE, "body", "to")
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email document should have been returned with only the "body" and "to"
-        // properties. The note document should have been returned with all of its properties.
-        AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        assertThat(documents).containsExactly(expectedNote, expectedEmail);
-    }
-
-    @Test
-    public void testQuery_projectionEmpty() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
-
-        // Query with type property paths {"Email", []}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email document should have been returned without any properties. The note document
-        // should have been returned with all of its properties.
-        AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .build();
-        GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        assertThat(documents).containsExactly(expectedNote, expectedEmail);
-    }
-
-    @Test
-    public void testQuery_projectionNonExistentType() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
-
-        // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection("NonExistentType", Collections.emptyList())
-                .addProjection(AppSearchEmail.SCHEMA_TYPE, "body", "to")
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email document should have been returned with only the "body" and "to" properties.
-        // The note document should have been returned with all of its properties.
-        AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        assertThat(documents).containsExactly(expectedNote, expectedEmail);
-    }
-
-    @Test
-    public void testQuery_wildcardProjection() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
-
-        // Query with type property paths {"*", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, "body", "to")
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email document should have been returned with only the "body" and "to"
-        // properties. The note document should have been returned with only the "body" property.
-        AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("body", "Note body").build();
-        assertThat(documents).containsExactly(expectedNote, expectedEmail);
-    }
-
-    @Test
-    public void testQuery_wildcardProjectionEmpty() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
-
-        // Query with type property paths {"*", []}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email and note documents should have been returned without any properties.
-        AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .build();
-        GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000).build();
-        assertThat(documents).containsExactly(expectedNote, expectedEmail);
-    }
-
-    @Test
-    public void testQuery_wildcardProjectionNonExistentType() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
-
-        // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection("NonExistentType", Collections.emptyList())
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, "body", "to")
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-
-        // The email document should have been returned with only the "body" and "to"
-        // properties. The note document should have been returned with only the "body" property.
-        AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("body", "Note body").build();
-        assertThat(documents).containsExactly(expectedNote, expectedEmail);
-    }
-
-    @Test
-    public void testQuery_twoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index a document to instance 1.
-        AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail1).build()));
-
-        // Index a document to instance 2.
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail2).build()));
-
-        // Query for instance 1.
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(inEmail1);
-
-        // Query for instance 2.
-        searchResults = mDb2.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(1);
-        assertThat(documents).containsExactly(inEmail2);
-    }
-
-    @Test
-    public void testSnippet() throws Exception {
-        // Schema registration
-        // TODO(tytytyww) add property for long and  double.
-        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .build()
-                ).build();
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(genericSchema).build()).get();
-
-        // Index a document
-        GenericDocument document =
-                new GenericDocument.Builder<>("uri", "Generic")
-                        .setNamespace("document")
-                        .setPropertyString("subject", "A commonly used fake word is foo. "
-                                + "Another nonsense word that’s used a lot is bar")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(document).build()));
-
-        // Query for the document
-        SearchResults searchResults = mDb1.query("foo",
-                new SearchSpec.Builder()
-                        .addSchemaType("Generic")
-                        .setSnippetCount(1)
-                        .setSnippetCountPerProperty(1)
-                        .setMaxSnippetSize(10)
-                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .build());
-        List<SearchResult> results = searchResults.getNextPage().get();
-        assertThat(results).hasSize(1);
-
-        List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatches();
-        assertThat(matchInfos).isNotNull();
-        assertThat(matchInfos).hasSize(1);
-        SearchResult.MatchInfo matchInfo = matchInfos.get(0);
-        assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
-                + "Another nonsense word that’s used a lot is bar");
-        assertThat(matchInfo.getExactMatchPosition()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/29,  /*upper=*/32));
-        assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
-        assertThat(matchInfo.getSnippetPosition()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/26,  /*upper=*/33));
-        assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
-    }
-
-    @Test
-    public void testRemove() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 2")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2).build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
-
-        // Delete the document
-        checkIsBatchResultSuccess(mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()));
-
-        // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1", "uri2").build())
-                .get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
-
-        // Test if we delete a nonexistent URI.
-        AppSearchBatchResult<String, Void> deleteResult = mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()).get();
-
-        assertThat(deleteResult.getFailures().get("uri1").getResultCode()).isEqualTo(
-                AppSearchResult.RESULT_NOT_FOUND);
-    }
-
-    @Test
-    public void testRemoveByQuery() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("foo")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("bar")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2).build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
-
-        // Delete the email 1 by query "foo"
-        mDb1.removeByQuery("foo",
-                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1", "uri2").build())
-                .get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
-
-        // Delete the email 2 by query "bar"
-        mDb1.removeByQuery("bar",
-                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
-        getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri2").build())
-                .get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri2").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-    }
-
-    @Test
-    public void testRemove_twoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-
-        // Can't delete in the other instance.
-        AppSearchBatchResult<String, Void> deleteResult = mDb2.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(deleteResult.getFailures().get("uri1").getResultCode()).isEqualTo(
-                AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-
-        // Delete the document
-        checkIsBatchResultSuccess(mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()));
-
-        // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Test if we delete a nonexistent URI.
-        deleteResult = mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(deleteResult.getFailures().get("uri1").getResultCode()).isEqualTo(
-                AppSearchResult.RESULT_NOT_FOUND);
-    }
-
-    @Test
-    public void testRemoveByTypes() throws Exception {
-        // Schema registration
-        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).addSchema(
-                        genericSchema).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 2")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        GenericDocument document1 =
-                new GenericDocument.Builder<>("uri3", "Generic").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2, document1)
-                        .build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1", "uri2",
-                "uri3")).hasSize(3);
-
-        // Delete the email type
-        mDb1.removeByQuery("",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
-                        .build())
-                .get();
-
-        // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1", "uri2", "uri3").build())
-                .get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getFailures().get("uri2").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getSuccesses().get("uri3")).isEqualTo(document1);
-    }
-
-    @Test
-    public void testRemoveByTypes_twoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 2")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
-
-        // Delete the email type in instance 1
-        mDb1.removeByQuery("",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
-                        .build())
-                .get();
-
-        // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Make sure it's still in instance 2.
-        getResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().addUri("uri2").build()).get();
-        assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
-    }
-
-    @Test
-    public void testRemoveByNamespace() throws Exception {
-        // Schema registration
-        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("foo")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .build()
-                ).build();
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).addSchema(
-                        genericSchema).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("email")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("email")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 2")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        GenericDocument document1 =
-                new GenericDocument.Builder<>("uri3", "Generic")
-                        .setNamespace("document")
-                        .setPropertyString("foo", "bar").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2, document1)
-                        .build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, /*namespace=*/"email", "uri1", "uri2")).hasSize(2);
-        assertThat(doGet(mDb1, /*namespace=*/"document", "uri3")).hasSize(1);
-
-        // Delete the email namespace
-        mDb1.removeByQuery("",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addNamespace("email")
-                        .build())
-                .get();
-
-        // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace("email")
-                        .addUri("uri1", "uri2").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getFailures().get("uri2").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace("document")
-                        .addUri("uri3").build()).get();
-        assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri3")).isEqualTo(document1);
-    }
-
-    @Test
-    public void testRemoveByNamespaces_twoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("email")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("email")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 2")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, /*namespace=*/"email", "uri1")).hasSize(1);
-        assertThat(doGet(mDb2, /*namespace=*/"email", "uri2")).hasSize(1);
-
-        // Delete the email namespace in instance 1
-        mDb1.removeByQuery("",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addNamespace("email")
-                        .build())
-                .get();
-
-        // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace("email")
-                        .addUri("uri1").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Make sure it's still in instance 2.
-        getResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().setNamespace("email")
-                        .addUri("uri2").build()).get();
-        assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
-    }
-
-    @Test
-    public void testRemoveAll_twoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 2")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
-
-        // Delete the all document in instance 1
-        mDb1.removeByQuery("",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .build())
-                .get();
-
-        // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Make sure it's still in instance 2.
-        getResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().addUri("uri2").build()).get();
-        assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
-    }
-
-    @Test
-    public void testRemoveAll_termMatchType() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 2")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        AppSearchEmail email3 =
-                new AppSearchEmail.Builder("uri3")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 3")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        AppSearchEmail email4 =
-                new AppSearchEmail.Builder("uri4")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example 4")
-                        .setBody("This is the body of the testPut second email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email3, email4).build()));
-
-        // Check the presence of the documents
-        SearchResults searchResults = mDb1.query("", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-        searchResults = mDb2.query("", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).hasSize(2);
-
-        // Delete the all document in instance 1 with TERM_MATCH_PREFIX
-        mDb1.removeByQuery("",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .build())
-                .get();
-        searchResults = mDb1.query("", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).isEmpty();
-
-        // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
-        mDb2.removeByQuery("",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .build())
-                .get();
-        searchResults = mDb2.query("", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).isEmpty();
-    }
-
-    @Test
-    public void testRemoveAllAfterEmpty() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // Index documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-
-        // Check the presence of the documents
-        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
-
-        // Remove the document
-        checkIsBatchResultSuccess(
-                mDb1.removeByUri(new RemoveByUriRequest.Builder()
-                        .setNamespace("namespace").addUri("uri1").build()));
-
-        // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-
-        // Delete the all documents
-        mDb1.removeByQuery(
-                "", new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
-                .get();
-
-        // Make sure it's still gone
-        getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
-                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-    }
-
-    @Test
-    public void testCloseAndReopen() throws Exception {
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-            .get();
-
-        // Index a document
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
-
-        // close and re-open the appSearchSession
-        mDb1.close();
-        mDb1 = createSearchSession(DB_NAME_1).get();
-
-        // Query for the document
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inEmail);
-    }
-
-    @Test
-    public void testCallAfterClose() throws Exception {
-
-        // Create a same-thread database by inject an executor which could help us maintain the
-        // execution order of those async tasks.
-        Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession sameThreadDb = createSearchSession(
-                "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
-
-        try {
-            // Schema registration -- just mutate something
-            sameThreadDb.setSchema(
-                    new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-            // Close the database. No further call will be allowed.
-            sameThreadDb.close();
-
-            // Try to query the closed database
-            // We are using the same-thread db here to make sure it has been closed.
-            IllegalStateException e = assertThrows(IllegalStateException.class, () ->
-                    sameThreadDb.query("query", new SearchSpec.Builder()
-                            .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                            .build()));
-            assertThat(e).hasMessageThat().contains("AppSearchSession has already been closed");
-        } finally {
-            // To clean the data that has been added in the test, need to re-open the session and
-            // set an empty schema.
-            AppSearchSession reopen = createSearchSession(
-                    "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
-            reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-        }
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GenericDocumentCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GenericDocumentCtsTest.java
deleted file mode 100644
index 4fa033a..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GenericDocumentCtsTest.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * 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 androidx.appsearch.app.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import androidx.appsearch.app.GenericDocument;
-
-import org.junit.Ignore;
-import org.junit.Test;
-
-public class GenericDocumentCtsTest {
-    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, (byte) 7};
-    private static final GenericDocument sDocumentProperties1 = new GenericDocument
-            .Builder<>("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
-            .setCreationTimestampMillis(12345L)
-            .build();
-    private static final GenericDocument sDocumentProperties2 = new GenericDocument
-            .Builder<>("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
-            .setCreationTimestampMillis(6789L)
-            .build();
-
-    @Test
-    public void testDocumentEquals_identical() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setTtlMillis(1L)
-                .setPropertyLong("longKey1", 1L, 2L, 3L)
-                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
-                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
-                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
-                .build();
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setTtlMillis(1L)
-                .setPropertyLong("longKey1", 1L, 2L, 3L)
-                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
-                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
-                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
-                .build();
-        assertThat(document1).isEqualTo(document2);
-        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
-    }
-
-    @Test
-    public void testDocumentEquals_differentOrder() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyLong("longKey1", 1L, 2L, 3L)
-                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
-                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
-                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
-                .build();
-
-        // Create second document with same parameter but different order.
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
-                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
-                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
-                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
-                .setPropertyLong("longKey1", 1L, 2L, 3L)
-                .build();
-        assertThat(document1).isEqualTo(document2);
-        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
-    }
-
-    @Test
-    public void testDocumentEquals_failure() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyLong("longKey1", 1L, 2L, 3L)
-                .build();
-
-        // Create second document with same order but different value.
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyLong("longKey1", 1L, 2L, 4L) // Different
-                .build();
-        assertThat(document1).isNotEqualTo(document2);
-        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
-    }
-
-    @Test
-    public void testDocumentEquals_repeatedFieldOrder_failure() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .build();
-
-        // Create second document with same order but different value.
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyBoolean("booleanKey1", true, true, false) // Different
-                .build();
-        assertThat(document1).isNotEqualTo(document2);
-        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
-    }
-
-    @Test
-    public void testDocumentGetSingleValue() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setScore(1)
-                .setTtlMillis(1L)
-                .setPropertyLong("longKey1", 1L)
-                .setPropertyDouble("doubleKey1", 1.0)
-                .setPropertyBoolean("booleanKey1", true)
-                .setPropertyString("stringKey1", "test-value1")
-                .setPropertyBytes("byteKey1", sByteArray1)
-                .setPropertyDocument("documentKey1", sDocumentProperties1)
-                .build();
-        assertThat(document.getUri()).isEqualTo("uri1");
-        assertThat(document.getTtlMillis()).isEqualTo(1L);
-        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
-        assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
-        assertThat(document.getScore()).isEqualTo(1);
-        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
-        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
-        assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
-        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
-        assertThat(document.getPropertyBytes("byteKey1"))
-                .asList().containsExactly((byte) 1, (byte) 2, (byte) 3);
-        assertThat(document.getPropertyDocument("documentKey1")).isEqualTo(sDocumentProperties1);
-    }
-
-    @Test
-    public void testDocumentGetArrayValues() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyLong("longKey1", 1L, 2L, 3L)
-                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
-                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
-                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
-                .build();
-
-        assertThat(document.getUri()).isEqualTo("uri1");
-        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
-        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L, 2L, 3L);
-        assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
-                .containsExactly(1.0, 2.0, 3.0);
-        assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
-                .containsExactly(true, false, true);
-        assertThat(document.getPropertyStringArray("stringKey1")).asList()
-                .containsExactly("test-value1", "test-value2", "test-value3");
-        assertThat(document.getPropertyBytesArray("byteKey1")).asList()
-                .containsExactly(sByteArray1, sByteArray2);
-        assertThat(document.getPropertyDocumentArray("documentKey1")).asList()
-                .containsExactly(sDocumentProperties1, sDocumentProperties2);
-    }
-
-    @Ignore
-    @Test
-    public void testDocument_toString() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setCreationTimestampMillis(5L)
-                .setPropertyLong("longKey1", 1L, 2L, 3L)
-                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .setPropertyString("stringKey1", "String1", "String2", "String3")
-                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
-                .setPropertyDocument("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() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
-                .setScore(1)
-                .setPropertyLong("longKey1", 1L)
-                .setPropertyBoolean("booleanKey1", true, false, true)
-                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
-                .build();
-
-        // Get a value for a key that doesn't exist
-        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(0.0);
-        assertThat(document.getPropertyDoubleArray("doubleKey1")).isNull();
-
-        // Get a value with a single element as an array and as a single value
-        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
-        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L);
-
-        // Get a value with multiple elements as an array and as a single value
-        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
-        assertThat(document.getPropertyStringArray("stringKey1")).asList()
-                .containsExactly("test-value1", "test-value2", "test-value3");
-
-        // Get a value of the wrong type
-        assertThat(document.getPropertyDouble("longKey1")).isEqualTo(0.0);
-        assertThat(document.getPropertyDoubleArray("longKey1")).isNull();
-    }
-
-    @Test
-    public void testDocumentInvalid() {
-        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("uri1", "schemaType1");
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> builder.setPropertyBoolean("test", new boolean[]{}));
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
deleted file mode 100644
index 0120153..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
+++ /dev/null
@@ -1,568 +0,0 @@
-/*
- * 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 androidx.appsearch.app.cts;
-
-import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
-import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.appsearch.app.AppSearchEmail;
-import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
-import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GlobalSearchSession;
-import androidx.appsearch.app.PutDocumentsRequest;
-import androidx.appsearch.app.SearchResult;
-import androidx.appsearch.app.SearchResults;
-import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.localstorage.LocalStorage;
-import androidx.test.core.app.ApplicationProvider;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public abstract class GlobalSearchSessionCtsTestBase {
-    private AppSearchSession mDb1;
-    private static final String DB_NAME_1 = LocalStorage.DEFAULT_DATABASE_NAME;
-    private AppSearchSession mDb2;
-    private static final String DB_NAME_2 = "testDb2";
-
-    private GlobalSearchSession mGlobalAppSearchManager;
-
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
-            @NonNull String dbName);
-    protected abstract ListenableFuture<GlobalSearchSession> createGlobalSearchSession();
-
-    @Before
-    public void setUp() throws Exception {
-        Context context = ApplicationProvider.getApplicationContext();
-
-        mDb1 = createSearchSession(DB_NAME_1).get();
-        mDb2 = createSearchSession(DB_NAME_2).get();
-
-        // Cleanup whatever documents may still exist in these databases. This is needed in
-        // addition to tearDown in case a test exited without completing properly.
-        cleanup();
-
-        mGlobalAppSearchManager = createGlobalSearchSession().get();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        // Cleanup whatever documents may still exist in these databases.
-        cleanup();
-    }
-
-    private void cleanup() throws Exception {
-        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-    }
-
-    private List<GenericDocument> snapshotResults(String queryExpression, SearchSpec spec)
-            throws Exception {
-        SearchResults searchResults = mGlobalAppSearchManager.query(queryExpression, spec);
-        return convertSearchResultsToDocuments(searchResults);
-    }
-
-    /**
-     * Asserts that the union of {@code addedDocuments} and {@code beforeDocuments} is exactly
-     * equivalent to {@code afterDocuments}. Order doesn't matter.
-     *
-     * @param beforeDocuments Documents that existed first.
-     * @param afterDocuments  The total collection of documents that should exist now.
-     * @param addedDocuments  The collection of documents that were expected to be added.
-     */
-    private void assertAddedBetweenSnapshots(List<? extends GenericDocument> beforeDocuments,
-            List<? extends GenericDocument> afterDocuments,
-            List<? extends GenericDocument> addedDocuments) {
-        List<GenericDocument> expectedDocuments = new ArrayList<>(beforeDocuments);
-        expectedDocuments.addAll(addedDocuments);
-        assertThat(afterDocuments).containsExactlyElementsIn(expectedDocuments);
-    }
-
-    @Test
-    public void testGlobalQuery_oneInstance() throws Exception {
-        // Snapshot what documents may already exist on the device.
-        SearchSpec exactSearchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build();
-        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
-        List<GenericDocument> beforeBodyEmailDocuments = snapshotResults("body email",
-                exactSearchSpec);
-
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-
-        // Index a document
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
-
-        // Query for the document
-        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
-        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
-                Collections.singletonList(inEmail));
-
-        // Multi-term query
-        List<GenericDocument> afterBodyEmailDocuments = snapshotResults("body email",
-                exactSearchSpec);
-        assertAddedBetweenSnapshots(beforeBodyEmailDocuments, afterBodyEmailDocuments,
-                Collections.singletonList(inEmail));
-    }
-
-    @Test
-    public void testGlobalQuery_twoInstances() throws Exception {
-        // Snapshot what documents may already exist on the device.
-        SearchSpec exactSearchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build();
-        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
-
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-
-        // Index a document to instance 1.
-        AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail1).build()));
-
-        // Index a document to instance 2.
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail2).build()));
-
-        // Query across all instances
-        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
-        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
-                ImmutableList.of(inEmail1, inEmail2));
-    }
-
-    @Test
-    public void testGlobalQuery_getNextPage() throws Exception {
-        // Snapshot what documents may already exist on the device.
-        SearchSpec exactSearchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build();
-        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
-
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-        List<AppSearchEmail> emailList = new ArrayList<>();
-        PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
-
-        // Index 31 documents
-        for (int i = 0; i < 31; i++) {
-            AppSearchEmail inEmail =
-                    new AppSearchEmail.Builder("uri" + i)
-                            .setFrom("from@example.com")
-                            .setTo("to1@example.com", "to2@example.com")
-                            .setSubject("testPut example")
-                            .setBody("This is the body of the testPut email")
-                            .build();
-            emailList.add(inEmail);
-            putDocumentsRequestBuilder.addGenericDocument(inEmail);
-        }
-        checkIsBatchResultSuccess(mDb1.putDocuments(putDocumentsRequestBuilder.build()));
-
-        // Set number of results per page is 7.
-        int pageSize = 7;
-        SearchResults searchResults = mGlobalAppSearchManager.query("body",
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultCountPerPage(pageSize)
-                        .build());
-        List<GenericDocument> documents = new ArrayList<>();
-
-        int pageNumber = 0;
-        List<SearchResult> results;
-
-        // keep loading next page until it's empty.
-        do {
-            results = searchResults.getNextPage().get();
-            ++pageNumber;
-            for (SearchResult result : results) {
-                documents.add(result.getDocument());
-            }
-        } while (results.size() > 0);
-
-        // check all document presents
-        assertAddedBetweenSnapshots(beforeBodyDocuments, documents, emailList);
-
-        int totalDocuments = beforeBodyDocuments.size() + documents.size();
-
-        // +1 for final empty page
-        int expectedPages = (int) Math.ceil(totalDocuments * 1.0 / pageSize) + 1;
-        assertThat(pageNumber).isEqualTo(expectedPages);
-    }
-
-    @Test
-    public void testGlobalQuery_acrossTypes() throws Exception {
-        // Snapshot what documents may already exist on the device.
-        SearchSpec exactSearchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build();
-        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
-
-        SearchSpec exactEmailSearchSpec =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
-                        .build();
-        List<GenericDocument> beforeBodyEmailDocuments = snapshotResults("body",
-                exactEmailSearchSpec);
-
-        // Schema registration
-        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("foo")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .build()
-                ).build();
-
-        // db1 has both "Generic" and "builtin:Email"
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(genericSchema).addSchema(AppSearchEmail.SCHEMA).build()).get();
-
-        // db2 only has "builtin:Email"
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-
-        // Index a generic document into db1
-        GenericDocument genericDocument = new GenericDocument.Builder<>("uri2", "Generic")
-                .setPropertyString("foo", "body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(genericDocument).build()));
-
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        // Put the email in both databases
-        checkIsBatchResultSuccess((mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email).build())));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
-
-        // Query for all documents across types
-        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
-        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
-                ImmutableList.of(genericDocument, email, email));
-
-        // Query only for email documents
-        List<GenericDocument> afterBodyEmailDocuments = snapshotResults("body",
-                exactEmailSearchSpec);
-        assertAddedBetweenSnapshots(beforeBodyEmailDocuments, afterBodyEmailDocuments,
-                ImmutableList.of(email, email));
-    }
-
-    @Test
-    public void testGlobalQuery_namespaceFilter() throws Exception {
-        // Snapshot what documents may already exist on the device.
-        SearchSpec exactSearchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build();
-        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
-
-        SearchSpec exactNamespace1SearchSpec =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addNamespace("namespace1")
-                        .build();
-        List<GenericDocument> beforeBodyNamespace1Documents = snapshotResults("body",
-                exactNamespace1SearchSpec);
-
-        // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-
-        // Index two documents
-        AppSearchEmail document1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace1")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(document1).build()));
-
-        AppSearchEmail document2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace2")
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(document2).build()));
-
-        // Query for all namespaces
-        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
-        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
-                ImmutableList.of(document1, document2));
-
-        // Query only for "namespace1"
-        List<GenericDocument> afterBodyNamespace1Documents = snapshotResults("body",
-                exactNamespace1SearchSpec);
-        assertAddedBetweenSnapshots(beforeBodyNamespace1Documents, afterBodyNamespace1Documents,
-                ImmutableList.of(document1));
-    }
-
-    // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
-    @Test
-    public void testGlobalQuery_projectionTwoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .build()).get();
-        mDb2.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Index one document in each database.
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1).build()));
-
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email2).build()));
-
-        // Query with type property paths {"Email", ["subject", "to"]}
-        List<GenericDocument> documents =
-                snapshotResults("body", new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addProjection(AppSearchEmail.SCHEMA_TYPE,
-                                "subject", "to")
-                        .build());
-
-        // The two email documents should have been returned with only the "subject" and "to"
-        // properties.
-        AppSearchEmail expected1 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .build();
-        AppSearchEmail expected2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .build();
-        assertThat(documents).containsExactly(expected1, expected2);
-    }
-
-    @Test
-    public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .build()).get();
-        mDb2.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Index one document in each database.
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1).build()));
-
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email2).build()));
-
-        // Query with type property paths {"Email", []}
-        List<GenericDocument> documents =
-                snapshotResults("body", new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addProjection(AppSearchEmail.SCHEMA_TYPE,
-                                Collections.emptyList())
-                        .build());
-
-        // The two email documents should have been returned without any properties.
-        AppSearchEmail expected1 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .build();
-        AppSearchEmail expected2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .build();
-        assertThat(documents).containsExactly(expected1, expected2);
-    }
-
-    @Test
-    public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
-        // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .build()).get();
-        mDb2.setSchema(
-                new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Index one document in each database.
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1).build()));
-
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("from@example.com")
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email2).build()));
-
-        // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
-        List<GenericDocument> documents =
-                snapshotResults("body", new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addProjection("NonExistentType", Collections.emptyList())
-                        .addProjection(AppSearchEmail.SCHEMA_TYPE, "subject", "to")
-                        .build());
-
-        // The two email documents should have been returned with only the "subject" and "to"
-        // properties.
-        AppSearchEmail expected1 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .build();
-        AppSearchEmail expected2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
-                        .setCreationTimestampMillis(1000)
-                        .setTo("to1@example.com", "to2@example.com")
-                        .setSubject("testPut example")
-                        .build();
-        assertThat(documents).containsExactly(expected1, expected2);
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SearchSpecCtsTest.java
deleted file mode 100644
index ad3c9a5..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SearchSpecCtsTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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 androidx.appsearch.app.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import androidx.appsearch.app.SearchSpec;
-
-import org.junit.Test;
-
-public class SearchSpecCtsTest {
-    @Test
-    public void buildSearchSpecWithoutTermMatchType() {
-        RuntimeException e = assertThrows(RuntimeException.class, () -> new SearchSpec.Builder()
-                .addSchemaType("testSchemaType")
-                .build());
-        assertThat(e).hasMessageThat().contains("Missing termMatchType field");
-    }
-
-    @Test
-    public void testBuildSearchSpec() {
-        SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addNamespace("namespace1", "namespace2")
-                .addSchemaType("schemaTypes1", "schemaTypes2")
-                .setSnippetCount(5)
-                .setSnippetCountPerProperty(10)
-                .setMaxSnippetSize(15)
-                .setResultCountPerPage(42)
-                .setOrder(SearchSpec.ORDER_ASCENDING)
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
-                .build();
-
-        assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
-        assertThat(searchSpec.getNamespaces())
-                .containsExactly("namespace1", "namespace2").inOrder();
-        assertThat(searchSpec.getSchemaTypes())
-                .containsExactly("schemaTypes1", "schemaTypes2").inOrder();
-        assertThat(searchSpec.getSnippetCount()).isEqualTo(5);
-        assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10);
-        assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15);
-        assertThat(searchSpec.getResultCountPerPage()).isEqualTo(42);
-        assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
-        assertThat(searchSpec.getRankingStrategy())
-                .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE);
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDataClass.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDataClass.java
deleted file mode 100644
index 1f8136d..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDataClass.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.
- */
-// @exportToFramework:skipFile()
-package androidx.appsearch.app.cts.customer;
-
-import androidx.appsearch.annotation.AppSearchDocument;
-import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
-
-@AppSearchDocument
-public final class EmailDataClass {
-    @AppSearchDocument.Uri
-    public String uri;
-
-    @AppSearchDocument.Property(indexingType = PropertyConfig.INDEXING_TYPE_PREFIXES)
-    public String subject;
-
-    @AppSearchDocument.Property(indexingType = PropertyConfig.INDEXING_TYPE_PREFIXES)
-    public String body;
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEmail.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchEmail.java
similarity index 63%
rename from appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEmail.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchEmail.java
index 9f7aa0e..651e6d3 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEmail.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchEmail.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 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,22 +14,20 @@
  * limitations under the License.
  */
 
-package androidx.appsearch.app;
-
+package androidx.appsearch.app.util;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.GenericDocument;
 
 /**
  * Encapsulates a {@link GenericDocument} that represent an email.
  *
  * <p>This class is a higher level implement of {@link GenericDocument}.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class AppSearchEmail extends GenericDocument {
     /** The name of the schema type for {@link AppSearchEmail} documents.*/
     public static final String SCHEMA_TYPE = "builtin:Email";
@@ -42,46 +40,40 @@
     private static final String KEY_BODY = "body";
 
     public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
-            .addProperty(new PropertyConfig.Builder(KEY_FROM)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            .addProperty(new StringPropertyConfig.Builder(KEY_FROM)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_TO)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_TO)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_CC)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_CC)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_BCC)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_BCC)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_SUBJECT)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_SUBJECT)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_BODY)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_BODY)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
             ).build();
@@ -160,69 +152,63 @@
     /**
      * The builder class for {@link AppSearchEmail}.
      */
-    public static class Builder extends GenericDocument.Builder<AppSearchEmail.Builder> {
-
+    public static class Builder extends GenericDocument.Builder<Builder> {
         /**
-         * Creates a new {@link AppSearchEmail.Builder}
+         * Creates a new {@link Builder}
          *
-         * @param uri The Uri of the Email.
+         * @param namespace The namespace of the Email.
+         * @param id The ID of the Email.
          */
-        public Builder(@NonNull String uri) {
-            super(uri, SCHEMA_TYPE);
+        public Builder(@NonNull String namespace, @NonNull String id) {
+            super(namespace, id, SCHEMA_TYPE);
         }
 
         /**
          * Sets the from address of {@link AppSearchEmail}
          */
         @NonNull
-        public AppSearchEmail.Builder setFrom(@NonNull String from) {
-            setPropertyString(KEY_FROM, from);
-            return this;
+        public Builder setFrom(@NonNull String from) {
+            return setPropertyString(KEY_FROM, from);
         }
 
         /**
          * Sets the destination address of {@link AppSearchEmail}
          */
         @NonNull
-        public AppSearchEmail.Builder setTo(@NonNull String... to) {
-            setPropertyString(KEY_TO, to);
-            return this;
+        public Builder setTo(@NonNull String... to) {
+            return setPropertyString(KEY_TO, to);
         }
 
         /**
          * Sets the CC list of {@link AppSearchEmail}
          */
         @NonNull
-        public AppSearchEmail.Builder setCc(@NonNull String... cc) {
-            setPropertyString(KEY_CC, cc);
-            return this;
+        public Builder setCc(@NonNull String... cc) {
+            return setPropertyString(KEY_CC, cc);
         }
 
         /**
          * Sets the BCC list of {@link AppSearchEmail}
          */
         @NonNull
-        public AppSearchEmail.Builder setBcc(@NonNull String... bcc) {
-            setPropertyString(KEY_BCC, bcc);
-            return this;
+        public Builder setBcc(@NonNull String... bcc) {
+            return setPropertyString(KEY_BCC, bcc);
         }
 
         /**
          * Sets the subject of {@link AppSearchEmail}
          */
         @NonNull
-        public AppSearchEmail.Builder setSubject(@NonNull String subject) {
-            setPropertyString(KEY_SUBJECT, subject);
-            return this;
+        public Builder setSubject(@NonNull String subject) {
+            return setPropertyString(KEY_SUBJECT, subject);
         }
 
         /**
          * Sets the body of {@link AppSearchEmail}
          */
         @NonNull
-        public AppSearchEmail.Builder setBody(@NonNull String body) {
-            setPropertyString(KEY_BODY, body);
-            return this;
+        public Builder setBody(@NonNull String body) {
+            return setPropertyString(KEY_BODY, body);
         }
 
         /** Builds the {@link AppSearchEmail} object. */
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
index e9cade2..6e27675 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
@@ -22,12 +22,13 @@
 import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByUriRequest;
+import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Future;
 
 public class AppSearchTestUtils {
@@ -41,30 +42,51 @@
     }
 
     public static List<GenericDocument> doGet(
-            AppSearchSession session, String namespace, String... uris) throws Exception {
+            AppSearchSession session, String namespace, String... ids) throws Exception {
         AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
-                session.getByUri(
-                        new GetByUriRequest.Builder()
-                                .setNamespace(namespace).addUri(uris).build()));
-        assertThat(result.getSuccesses()).hasSize(uris.length);
+                session.getByDocumentId(
+                        new GetByDocumentIdRequest.Builder(namespace).addIds(ids).build()));
+        assertThat(result.getSuccesses()).hasSize(ids.length);
         assertThat(result.getFailures()).isEmpty();
-        List<GenericDocument> list = new ArrayList<>(uris.length);
-        for (String uri : uris) {
-            list.add(result.getSuccesses().get(uri));
+        List<GenericDocument> list = new ArrayList<>(ids.length);
+        for (String id : ids) {
+            list.add(result.getSuccesses().get(id));
+        }
+        return list;
+    }
+
+    public static List<GenericDocument> doGet(
+            AppSearchSession session, GetByDocumentIdRequest request) throws Exception {
+        AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
+                session.getByDocumentId(request));
+        Set<String> ids = request.getIds();
+        assertThat(result.getSuccesses()).hasSize(ids.size());
+        assertThat(result.getFailures()).isEmpty();
+        List<GenericDocument> list = new ArrayList<>(ids.size());
+        for (String id : ids) {
+            list.add(result.getSuccesses().get(id));
         }
         return list;
     }
 
     public static List<GenericDocument> convertSearchResultsToDocuments(SearchResults searchResults)
             throws Exception {
-        List<SearchResult> results = searchResults.getNextPage().get();
-        List<GenericDocument> documents = new ArrayList<>();
-        while (results.size() > 0) {
-            for (SearchResult result : results) {
-                documents.add(result.getDocument());
-            }
-            results = searchResults.getNextPage().get();
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        List<GenericDocument> documents = new ArrayList<>(results.size());
+        for (SearchResult result : results) {
+            documents.add(result.getGenericDocument());
         }
         return documents;
     }
+
+    public static List<SearchResult> retrieveAllSearchResults(SearchResults searchResults)
+            throws Exception {
+        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> results = new ArrayList<>();
+        while (!page.isEmpty()) {
+            results.addAll(page);
+            page = searchResults.getNextPage().get();
+        }
+        return results;
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchBatchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchBatchResultCtsTest.java
new file mode 100644
index 0000000..0f8a50e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchBatchResultCtsTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchResult;
+
+import org.junit.Test;
+
+public class AppSearchBatchResultCtsTest {
+    @Test
+    public void testIsSuccess_true() {
+        AppSearchBatchResult<String, Integer> result =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setResult("keySuccess3", AppSearchResult.newSuccessfulResult(3))
+                        .build();
+        assertThat(result.isSuccess()).isTrue();
+    }
+
+    @Test
+    public void testIsSuccess_false() {
+        AppSearchBatchResult<String, Integer> result1 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setFailure(
+                                "keyFailure1", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .build();
+
+        AppSearchBatchResult<String, Integer> result2 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setResult(
+                                "keyFailure3",
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"))
+                        .build();
+
+        assertThat(result1.isSuccess()).isFalse();
+        assertThat(result2.isSuccess()).isFalse();
+    }
+
+    @Test
+    public void testIsSuccess_replace() {
+        AppSearchBatchResult<String, Integer> result1 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("key", 1)
+                        .setFailure("key", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .build();
+
+        AppSearchBatchResult<String, Integer> result2 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setFailure("key", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .setSuccess("key", 1)
+                        .build();
+
+        assertThat(result1.isSuccess()).isFalse();
+        assertThat(result2.isSuccess()).isTrue();
+    }
+
+    @Test
+    public void testGetters() {
+        AppSearchBatchResult<String, Integer> result =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setFailure(
+                                "keyFailure1", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .setFailure(
+                                "keyFailure2", AppSearchResult.RESULT_INTERNAL_ERROR, "message2")
+                        .setResult("keySuccess3", AppSearchResult.newSuccessfulResult(3))
+                        .setResult(
+                                "keyFailure3",
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"))
+                        .build();
+
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getSuccesses()).containsExactly(
+                "keySuccess1", 1, "keySuccess2", 2, "keySuccess3", 3);
+        assertThat(result.getFailures()).containsExactly(
+                "keyFailure1",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_UNKNOWN_ERROR, "message1"),
+                "keyFailure2",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, "message2"),
+                "keyFailure3",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"));
+        assertThat(result.getAll()).containsExactly(
+                "keySuccess1", AppSearchResult.newSuccessfulResult(1),
+                "keySuccess2", AppSearchResult.newSuccessfulResult(2),
+                "keySuccess3", AppSearchResult.newSuccessfulResult(3),
+                "keyFailure1",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_UNKNOWN_ERROR, "message1"),
+                "keyFailure2",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, "message2"),
+                "keyFailure3",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"));
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchMigratorTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchMigratorTest.java
new file mode 100644
index 0000000..bf9ad8f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchMigratorTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+
+import org.junit.Test;
+
+public class AppSearchMigratorTest {
+
+    @Test
+    public void testOnUpgrade() {
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setCreationTimestampMillis(document.getCreationTimestampMillis())
+                        .setScore(document.getScore())
+                        .setTtlMillis(document.getTtlMillis())
+                        .setPropertyString("migration",
+                                "Upgrade the document from version " + currentVersion
+                                        + " to version " + finalVersion)
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        GenericDocument input = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L).build();
+
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L)
+                .setPropertyString("migration",
+                        "Upgrade the document from version 3 to version 5")
+                .build();
+
+        GenericDocument output = migrator.onUpgrade(/*currentVersion=*/3,
+                /*finalVersion=*/5, input);
+        assertThat(output).isEqualTo(expected);
+    }
+
+    @Test
+    public void testOnDowngrade() {
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setCreationTimestampMillis(document.getCreationTimestampMillis())
+                        .setScore(document.getScore())
+                        .setTtlMillis(document.getTtlMillis())
+                        .setPropertyString("migration",
+                                "Downgrade the document from version " + currentVersion
+                                        + " to version " + finalVersion)
+                        .build();
+            }
+        };
+
+        GenericDocument input = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L).build();
+
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L)
+                .setPropertyString("migration",
+                        "Downgrade the document from version 6 to version 4")
+                .build();
+
+        GenericDocument output = migrator.onDowngrade(/*currentVersion=*/6,
+                /*finalVersion=*/4, input);
+        assertThat(output).isEqualTo(expected);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
similarity index 98%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchResultCtsTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
index 5899068..91dacdb 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.appsearch.app.cts;
+package androidx.appsearch.cts.app;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
new file mode 100644
index 0000000..2ae353a
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -0,0 +1,371 @@
+/*
+ * 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 androidx.appsearch.cts.app;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.util.AppSearchEmail;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class AppSearchSchemaCtsTest {
+    @Test
+    public void testInvalidEnums() {
+        StringPropertyConfig.Builder builder = new StringPropertyConfig.Builder("test");
+        assertThrows(IllegalArgumentException.class, () -> builder.setCardinality(99));
+    }
+
+    @Test
+    public void testDefaultValues() {
+        StringPropertyConfig builder = new StringPropertyConfig.Builder("test").build();
+        assertThat(builder.getIndexingType()).isEqualTo(StringPropertyConfig.INDEXING_TYPE_NONE);
+        assertThat(builder.getTokenizerType()).isEqualTo(StringPropertyConfig.TOKENIZER_TYPE_NONE);
+        assertThat(builder.getCardinality()).isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
+    }
+
+    @Test
+    public void testDuplicateProperties() {
+        AppSearchSchema.Builder builder = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                );
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> builder.addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()));
+        assertThat(e).hasMessageThat().contains("Property defined more than once: subject");
+    }
+
+    @Test
+    public void testEquals_identical() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_differentOrder() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build()
+                ).build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure_differentProperty() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure_differentOrder() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        // Order of 'body' and 'subject' has been switched
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testPropertyConfig() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Test")
+                .addProperty(new StringPropertyConfig.Builder("string")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        "document", AppSearchEmail.SCHEMA_TYPE)
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+
+        assertThat(schema.getSchemaType()).isEqualTo("Test");
+        List<PropertyConfig> properties = schema.getProperties();
+        assertThat(properties).hasSize(6);
+
+        assertThat(properties.get(0).getName()).isEqualTo("string");
+        assertThat(properties.get(0).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(((StringPropertyConfig) properties.get(0)).getIndexingType())
+                .isEqualTo(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+        assertThat(((StringPropertyConfig) properties.get(0)).getTokenizerType())
+                .isEqualTo(StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
+
+        assertThat(properties.get(1).getName()).isEqualTo("long");
+        assertThat(properties.get(1).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(properties.get(1)).isInstanceOf(AppSearchSchema.LongPropertyConfig.class);
+
+        assertThat(properties.get(2).getName()).isEqualTo("double");
+        assertThat(properties.get(2).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(properties.get(2)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
+
+        assertThat(properties.get(3).getName()).isEqualTo("boolean");
+        assertThat(properties.get(3).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
+
+        assertThat(properties.get(4).getName()).isEqualTo("bytes");
+        assertThat(properties.get(4).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
+
+        assertThat(properties.get(5).getName()).isEqualTo("document");
+        assertThat(properties.get(5).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5)).getSchemaType())
+                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5))
+                .shouldIndexNestedProperties()).isEqualTo(true);
+    }
+
+    @Test
+    public void testInvalidStringPropertyConfigsTokenizerNone() {
+        // Everything should work fine with the defaults.
+        final StringPropertyConfig.Builder builder =
+                new StringPropertyConfig.Builder("property");
+        assertThat(builder.build()).isNotNull();
+
+        // Setting an indexing type other NONE with the default tokenizer type (NONE) should fail.
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+        assertThrows(IllegalStateException.class, () -> builder.build());
+
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES);
+        assertThrows(IllegalStateException.class, () -> builder.build());
+
+        // Explicitly setting the default should work fine.
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE);
+        assertThat(builder.build()).isNotNull();
+
+        // Explicitly setting the default tokenizer type should result in the same behavior.
+        builder.setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+        assertThrows(IllegalStateException.class, () -> builder.build());
+
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES);
+        assertThrows(IllegalStateException.class, () -> builder.build());
+
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE);
+        assertThat(builder.build()).isNotNull();
+    }
+
+    @Test
+    public void testInvalidStringPropertyConfigsTokenizerPlain() {
+        // Setting indexing type to be NONE with tokenizer type PLAIN should fail. Regardless of
+        // whether NONE is set explicitly or just kept as default.
+        final StringPropertyConfig.Builder builder =
+                new StringPropertyConfig.Builder("property")
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
+        assertThrows(IllegalStateException.class, () -> builder.build());
+
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE);
+        assertThrows(IllegalStateException.class, () -> builder.build());
+
+        // Setting indexing type to be something other than NONE with tokenizer type PLAIN should
+        // be just fine.
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+        assertThat(builder.build()).isNotNull();
+
+        builder.setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES);
+        assertThat(builder.build()).isNotNull();
+    }
+
+    @Test
+    public void testAppSearchSchema_toString() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new StringPropertyConfig.Builder("string1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("string2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("string3")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        "document", AppSearchEmail.SCHEMA_TYPE)
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+
+        String schemaString = schema.toString();
+
+        String expectedString = "{\n"
+                + "  schemaType: \"testSchema\",\n"
+                + "  properties: [\n"
+                + "    {\n"
+                + "      name: \"boolean\",\n"
+                + "      cardinality: CARDINALITY_REQUIRED,\n"
+                + "      dataType: DATA_TYPE_BOOLEAN,\n"
+                + "    },\n"
+                + "    {\n"
+                + "      name: \"bytes\",\n"
+                + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                + "      dataType: DATA_TYPE_BYTES,\n"
+                + "    },\n"
+                + "    {\n"
+                + "      name: \"document\",\n"
+                + "      shouldIndexNestedProperties: true,\n"
+                + "      schemaType: \"builtin:Email\",\n"
+                + "      cardinality: CARDINALITY_REPEATED,\n"
+                + "      dataType: DATA_TYPE_DOCUMENT,\n"
+                + "    },\n"
+                + "    {\n"
+                + "      name: \"double\",\n"
+                + "      cardinality: CARDINALITY_REPEATED,\n"
+                + "      dataType: DATA_TYPE_DOUBLE,\n"
+                + "    },\n"
+                + "    {\n"
+                + "      name: \"long\",\n"
+                + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                + "      dataType: DATA_TYPE_LONG,\n"
+                + "    },\n"
+                + "    {\n"
+                + "      name: \"string1\",\n"
+                + "      indexingType: INDEXING_TYPE_NONE,\n"
+                + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                + "      cardinality: CARDINALITY_REQUIRED,\n"
+                + "      dataType: DATA_TYPE_STRING,\n"
+                + "    },\n"
+                + "    {\n"
+                + "      name: \"string2\",\n"
+                + "      indexingType: INDEXING_TYPE_EXACT_TERMS,\n"
+                + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+                + "      cardinality: CARDINALITY_REQUIRED,\n"
+                + "      dataType: DATA_TYPE_STRING,\n"
+                + "    },\n"
+                + "    {\n"
+                + "      name: \"string3\",\n"
+                + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
+                + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+                + "      cardinality: CARDINALITY_REQUIRED,\n"
+                + "      dataType: DATA_TYPE_STRING,\n"
+                + "    }\n"
+                + "  ]\n"
+                + "}";
+
+        assertThat(schemaString).isEqualTo(expectedString);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
new file mode 100644
index 0000000..ddcae47
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
@@ -0,0 +1,1406 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static androidx.appsearch.app.AppSearchResult.RESULT_NOT_FOUND;
+import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
+import static androidx.appsearch.app.util.AppSearchTestUtils.doGet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/*
+ * For schema migration, we have 4 factors
+ * A. is ForceOverride set to true?
+ * B. is the schema change backwards compatible?
+ * C. is shouldTrigger return true?
+ * D. is there a migration triggered for each incompatible type and no deleted types?
+ * If B is true then D could never be false, so that will give us 12 combinations.
+ *
+ *                                Trigger       Delete      First            Second
+ * A      B       C       D       Migration     Types       SetSchema        SetSchema
+ * TRUE   TRUE    TRUE    TRUE    Yes                       succeeds         succeeds(noop)
+ * TRUE   TRUE    FALSE   TRUE                              succeeds         succeeds(noop)
+ * TRUE   FALSE   TRUE    TRUE    Yes                       fail             succeeds
+ * TRUE   FALSE   TRUE    FALSE   Yes           Yes         fail             succeeds
+ * TRUE   FALSE   FALSE   TRUE                  Yes         fail             succeeds
+ * TRUE   FALSE   FALSE   FALSE                 Yes         fail             succeeds
+ * FALSE  TRUE    TRUE    TRUE    Yes                       succeeds         succeeds(noop)
+ * FALSE  TRUE    FALSE   TRUE                              succeeds         succeeds(noop)
+ * FALSE  FALSE   TRUE    TRUE    Yes                       fail             succeeds
+ * FALSE  FALSE   TRUE    FALSE   Yes                       fail             throw error
+ * FALSE  FALSE   FALSE   TRUE    Impossible case, migrators are inactivity
+ * FALSE  FALSE   FALSE   FALSE                             fail             throw error
+ */
+public abstract class AppSearchSchemaMigrationCtsTestBase {
+
+    private static final String DB_NAME = "";
+    private static final long DOCUMENT_CREATION_TIME = 12345L;
+    private static final Migrator ACTIVE_NOOP_MIGRATOR = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return true;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+    private static final Migrator INACTIVE_MIGRATOR = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return false;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+
+    private AppSearchSession mDb;
+
+    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull String dbName);
+
+    @Before
+    public void setUp() throws Exception {
+        mDb = createSearchSession(DB_NAME).get();
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).build()).get();
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id0", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id0", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_B_C_D() throws Exception {
+        // create a backwards compatible schema and update the version
+        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_B_NC_D() throws Exception {
+        // create a backwards compatible schema but don't update the version
+        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_C_D() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_C_ND() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                        .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                        .setForceOverride(true)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_NC_D() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_NC_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_NC_ND() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                        .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_B_C_D() throws Exception {
+        // create a backwards compatible schema and update the version
+        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_B_NC_D() throws Exception {
+        // create a backwards compatible schema but don't update the version
+        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_C_D() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_C_ND() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema $B_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(
+                        new SetSchemaRequest.Builder().addSchemas($B_C_Schema)
+                                .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                                .setVersion(2)     // upgrade version
+                                .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_NC_ND() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(
+                        new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                                .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                                .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+        GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "testSchema")
+                .setPropertyString("subject", "testPut example2")
+                .setPropertyString("To", "testTo example2")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null, "id2", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // set the new schema to AppSearch, the first document will be migrated successfully but the
+        // second one will be failed.
+
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getId().equals("id2")) {
+                    return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                            document.getSchemaType())
+                            .setPropertyString("subject", "testPut example2")
+                            .setPropertyString("to",
+                                    "Expect to fail, property not in the schema")
+                            .build();
+                }
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setPropertyString("subject", "testPut example1 migrated")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Downgrade should not be triggered for this test");
+            }
+        };
+
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+
+        // Check the schema has been saved
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes())
+                .containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("testSchema");
+
+        // Check migrate the first document is success
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1 migrated")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
+
+        // Check migrate the second document is fail.
+        assertThat(setSchemaResponse.getMigrationFailures()).hasSize(1);
+        SetSchemaResponse.MigrationFailure migrationFailure =
+                setSchemaResponse.getMigrationFailures().get(0);
+        assertThat(migrationFailure.getNamespace()).isEqualTo("namespace");
+        assertThat(migrationFailure.getSchemaType()).isEqualTo("testSchema");
+        assertThat(migrationFailure.getDocumentId()).isEqualTo("id2");
+
+        AppSearchResult<Void> actualResult = migrationFailure.getAppSearchResult();
+        assertThat(actualResult.isSuccess()).isFalse();
+        assertThat(actualResult.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
+        assertThat(actualResult.getErrorMessage())
+                .contains("Property config 'to' not found for key");
+    }
+
+    @Test
+    public void testSchemaMigration_downgrade() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // set the new schema to AppSearch
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Upgrade should not be triggered for this test");
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setPropertyString("subject", "testPut example1 migrated")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+            }
+        };
+
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(1)     // downgrade version
+                        .build()).get();
+
+        // Check the schema has been saved
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes())
+                .containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("testSchema");
+
+        // Check migrate is success
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1 migrated")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_sameVersion() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type with the same version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // set the new schema to AppSearch
+        Migrator migrator = new Migrator() {
+
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Upgrade should not be triggered for this test");
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Downgrade should not be triggered for this test");
+            }
+        };
+
+        // SetSchema with forceOverride=false
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(3)     // same version
+                        .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+
+        // SetSchema with forceOverride=true
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(3)     // same version
+                        .setForceOverride(true).build()).get();
+
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes())
+                .containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+    }
+
+    @Test
+    public void testSchemaMigration_noMigration() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).setVersion(2).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // Set start version to be 3 means we won't trigger migration for 2.
+        Migrator migrator = new Migrator() {
+
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion > 2 && currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Upgrade should not be triggered for this test");
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Downgrade should not be triggered for this test");
+            }
+        };
+
+        // SetSchema with forceOverride=false
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(4)     // upgrade version
+                        .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration_sourceToNowhere() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema schema = new AppSearchSchema.Builder("sourceSchema")
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).build()).get();
+
+        // save a doc to the source type
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id1", "sourceSchema")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        Migrator migrator_sourceToNowhere = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(
+                        "zombieNamespace", "zombieId", "nonExistSchema")
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // SetSchema with forceOverride=false
+        // Source type exist, destination type doesn't exist.
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder()
+                        .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
+                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setVersion(2).build())   // upgrade version
+                        .get());
+        assertThat(exception).hasMessageThat().contains(
+                "Receive a migrated document with schema type: nonExistSchema. "
+                        + "But the schema types doesn't exist in the request");
+
+        // SetSchema with forceOverride=true
+        // Source type exist, destination type doesn't exist.
+        exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder()
+                        .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
+                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setForceOverride(true)
+                        .setVersion(2).build())   // upgrade version
+                        .get());
+        assertThat(exception).hasMessageThat().contains(
+                "Receive a migrated document with schema type: nonExistSchema. "
+                        + "But the schema types doesn't exist in the request");
+    }
+
+    @Test
+    public void testSchemaMigration_nowhereToDestination() throws Exception {
+        // set the destination schema to AppSearch
+        AppSearchSchema destinationSchema =
+                new AppSearchSchema.Builder("destinationSchema").build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(destinationSchema).setForceOverride(true).build()).get();
+
+        Migrator migrator_nowhereToDestination = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+
+        // Source type doesn't exist, destination type exist. Since source type doesn't exist,
+        // no matter force override or not, the migrator won't be invoked
+        // SetSchema with forceOverride=false
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
+                        .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
+                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setVersion(2) //  upgrade version
+                        .build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+        // SetSchema with forceOverride=true
+        setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
+                        .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
+                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setVersion(2) //  upgrade version
+                        .setForceOverride(true).build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaMigration_nowhereToNowhere() throws Exception {
+        // set empty schema
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .setForceOverride(true).build()).get();
+        Migrator migrator_nowhereToNowhere = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+
+        // Source type doesn't exist, destination type exist. Since source type doesn't exist,
+        // no matter force override or not, the migrator won't be invoked
+        // SetSchema with forceOverride=false
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder()
+                        .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
+                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setVersion(2)  //  upgrade version
+                        .build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+        // SetSchema with forceOverride=true
+        setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder()
+                        .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
+                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setVersion(2) //  upgrade version
+                        .setForceOverride(true).build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaMigration_toAnotherType() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema sourceSchema = new AppSearchSchema.Builder("sourceSchema")
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(sourceSchema).setForceOverride(true).build()).get();
+
+        // save a doc to the source type
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id1", "sourceSchema").build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create the destination type and migrator
+        AppSearchSchema destinationSchema = new AppSearchSchema.Builder("destinationSchema")
+                .build();
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>("namespace",
+                        document.getId(),
+                        "destinationSchema")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(destinationSchema)
+                .setMigrator("sourceSchema", migrator)
+                .setForceOverride(false)
+                .setVersion(2) //  upgrade version
+                .build()).get();
+        assertThat(setSchemaResponse.getDeletedTypes())
+                .containsExactly("sourceSchema");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("sourceSchema");
+
+        // Check successfully migrate the doc to the destination type
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id1", "destinationSchema")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_toMultipleDestinationType() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema sourceSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("Age")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(sourceSchema).setForceOverride(true).build()).get();
+
+        // save a child and an adult to the Person type
+        GenericDocument childDoc = new GenericDocument.Builder<>(
+                "namespace", "Person1", "Person")
+                .setPropertyLong("Age", 6).build();
+        GenericDocument adultDoc = new GenericDocument.Builder<>(
+                "namespace", "Person2", "Person")
+                .setPropertyLong("Age", 36).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(childDoc, adultDoc).build()));
+        assertThat(result.getSuccesses()).containsExactly("Person1", null, "Person2", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create the migrator
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getPropertyLong("Age") < 21) {
+                    return new GenericDocument.Builder<>(
+                            "namespace", "child-id", "Child")
+                            .setPropertyLong("Age", document.getPropertyLong("Age"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                } else {
+                    return new GenericDocument.Builder<>(
+                            "namespace", "adult-id", "Adult")
+                            .setPropertyLong("Age", document.getPropertyLong("Age"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                }
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // create adult and child schema
+        AppSearchSchema adultSchema = new AppSearchSchema.Builder("Adult")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("Age")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema childSchema = new AppSearchSchema.Builder("Child")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("Age")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(adultSchema, childSchema)
+                .setMigrator("Person", migrator)
+                .setForceOverride(false)
+                .setVersion(2) //  upgrade version
+                .build()).get();
+        assertThat(setSchemaResponse.getDeletedTypes())
+                .containsExactly("Person");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("Person");
+
+        // Check successfully migrate the child doc
+        GenericDocument expectedInChild = new GenericDocument.Builder<>(
+                "namespace", "child-id", "Child")
+                .setPropertyLong("Age", 6)
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "child-id"))
+                .containsExactly(expectedInChild);
+
+        // Check successfully migrate the adult doc
+        GenericDocument expectedInAdult = new GenericDocument.Builder<>(
+                "namespace", "adult-id", "Adult")
+                .setPropertyLong("Age", 36)
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "adult-id"))
+                .containsExactly(expectedInAdult);
+    }
+
+    @Test
+    public void testSchemaMigration_loadTest() throws Exception {
+        // set the two source type A & B to AppSearch
+        AppSearchSchema sourceSchemaA = new AppSearchSchema.Builder("schemaA")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema sourceSchemaB = new AppSearchSchema.Builder("schemaB")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(sourceSchemaA, sourceSchemaB).setForceOverride(true).build()).get();
+
+        // save 100 docs to each type
+        PutDocumentsRequest.Builder putRequestBuilder = new PutDocumentsRequest.Builder();
+        for (int i = 0; i < 100; i++) {
+            GenericDocument docInA = new GenericDocument.Builder<>(
+                    "namespace", "idA-" + i, "schemaA")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            GenericDocument docInB = new GenericDocument.Builder<>(
+                    "namespace", "idB-" + i, "schemaB")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            putRequestBuilder.addGenericDocuments(docInA, docInB);
+        }
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                putRequestBuilder.build()));
+        assertThat(result.getFailures()).isEmpty();
+
+        // create three destination types B, C & D
+        AppSearchSchema destinationSchemaB = new AppSearchSchema.Builder("schemaB")
+                .addProperty(
+                        new AppSearchSchema.LongPropertyConfig.Builder("numNewProperty")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema destinationSchemaC = new AppSearchSchema.Builder("schemaC")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema destinationSchemaD = new AppSearchSchema.Builder("schemaD")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+
+        // Create an active migrator for type A which will migrate first 50 docs to C and second
+        // 50 docs to D
+        Migrator migratorA = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getPropertyLong("num") < 50) {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destC", "schemaC")
+                            .setPropertyLong("num", document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                } else {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destD", "schemaD")
+                            .setPropertyLong("num", document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                }
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // Create an active migrator for type B which will migrate first 50 docs to B and second
+        // 50 docs to D
+        Migrator migratorB = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getPropertyLong("num") < 50) {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destB", "schemaB")
+                            .setPropertyLong("numNewProperty",
+                                    document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                } else {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destD", "schemaD")
+                            .setPropertyLong("num", document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                }
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(destinationSchemaB, destinationSchemaC, destinationSchemaD)
+                .setMigrator("schemaA", migratorA)
+                .setMigrator("schemaB", migratorB)
+                .setForceOverride(false)
+                .setVersion(2)    // upgrade version
+                .build()).get();
+        assertThat(setSchemaResponse.getDeletedTypes())
+                .containsExactly("schemaA");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("schemaB");
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("schemaA", "schemaB");
+
+        // generate expected documents
+        List<GenericDocument> expectedDocs = new ArrayList<>();
+        for (int i = 0; i < 50; i++) {
+            GenericDocument docAToC = new GenericDocument.Builder<>(
+                    "namespace", "idA-" + i + "-destC", "schemaC")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            GenericDocument docBToB = new GenericDocument.Builder<>(
+                    "namespace", "idB-" + i + "-destB", "schemaB")
+                    .setPropertyLong("numNewProperty", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            expectedDocs.add(docAToC);
+            expectedDocs.add(docBToB);
+        }
+
+        for (int i = 50; i < 100; i++) {
+            GenericDocument docAToD = new GenericDocument.Builder<>(
+                    "namespace", "idA-" + i + "-destD", "schemaD")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            GenericDocument docBToD = new GenericDocument.Builder<>(
+                    "namespace", "idB-" + i + "-destD", "schemaD")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            expectedDocs.add(docAToD);
+            expectedDocs.add(docBToD);
+        }
+        //query all documents and compare
+        SearchResults searchResults = mDb.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactlyElementsIn(expectedDocs);
+    }
+
+    //*************************** Multi-step migration tests   ******************************
+    // Version structure and how version bumps:
+    // Version 1: Start - typeA docs contains "subject" property.
+    // Version 2: typeA docs get new "body" property, contains "subject" and "body" now.
+    // Version 3: typeA docs is migrated to typeB, typeA docs got removed, typeB doc contains
+    //            "subject" and "body" property.
+    // Version 4: typeB docs remove "subject" property, contains only "body" now.
+
+    // Create a multi-step migrator for A, which could migrate version 1-3 to 4.
+    private static final Migrator MULTI_STEP_MIGRATOR_A = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return currentVersion < 3;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            GenericDocument.Builder docBuilder =
+                    new GenericDocument.Builder<>("namespace", "id", "TypeB")
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME);
+            if (currentVersion == 2) {
+                docBuilder.setPropertyString("body", document.getPropertyString("body"));
+            } else {
+                docBuilder.setPropertyString("body",
+                        "new content for the newly added 'body' property");
+            }
+            return docBuilder.build();
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+
+    // create a multi-step migrator for B, which could migrate version 1-3 to 4.
+    private static final Migrator MULTI_STEP_MIGRATOR_B = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return currentVersion == 3;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return new GenericDocument.Builder<>("namespace", "id", "TypeB")
+                    .setPropertyString("body", document.getPropertyString("body"))
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                    .build();
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+
+    // create a setSchemaRequest, which could migrate version 1-3 to 4.
+    private static final SetSchemaRequest MULTI_STEP_REQUEST = new SetSchemaRequest.Builder()
+            .addSchemas(new AppSearchSchema.Builder("TypeB")
+                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                            .setIndexingType(
+                                    AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                            .setTokenizerType(
+                                    AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                            .build())
+                    .build())
+            .setMigrator("TypeA", MULTI_STEP_MIGRATOR_A)
+            .setMigrator("TypeB", MULTI_STEP_MIGRATOR_B)
+            .setVersion(4)
+            .build();
+
+    @Test
+    public void testSchemaMigration_multiStep1To4() throws Exception {
+        // set version 1 to the database, only contain TypeA
+        AppSearchSchema typeA = new AppSearchSchema.Builder("TypeA")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(typeA).setForceOverride(true).setVersion(1).build()).get();
+
+        // save a doc to version 1.
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeA")
+                .setPropertyString("subject", "subject")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // update to version 4.
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
+
+        // Create expected doc. Since we started at version 1 and migrated to version 4:
+        // 1: A 'body' property should have been added with "new content for the newly added 'body'
+        //    property"
+        // 2: The type should have been changed from 'TypeA' to 'TypeB'
+        // 3: The 'subject' property should have been removed
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("body", "new content for the newly added 'body' property")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_multiStep2To4() throws Exception {
+        // set version 2 to the database, only contain TypeA with a new property
+        AppSearchSchema typeA = new AppSearchSchema.Builder("TypeA")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(typeA).setForceOverride(true).setVersion(2).build()).get();
+
+        // save a doc to version 2.
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeA")
+                .setPropertyString("subject", "subject")
+                .setPropertyString("body", "bodyFromA")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // update to version 4.
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
+
+        // create expected doc, body exists in type A of version 2
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("body", "bodyFromA")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_multiStep3To4() throws Exception {
+        // set version 3 to the database, only contain TypeB
+        AppSearchSchema typeA = new AppSearchSchema.Builder("TypeB")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(typeA).setForceOverride(true).setVersion(3).build()).get();
+
+        // save a doc to version 2.
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("subject", "subject")
+                .setPropertyString("body", "bodyFromB")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // update to version 4.
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("TypeB");
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeB");
+
+        // create expected doc, body exists in type A of version 3
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("body", "bodyFromB")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
similarity index 60%
copy from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
copy to appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
index 4586ca1..6525ddd 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 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,32 +14,22 @@
  * limitations under the License.
  */
 // @exportToFramework:skipFile()
-package androidx.appsearch.app.cts;
+package androidx.appsearch.cts.app;
 
 import android.content.Context;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-// TODO(b/175801531): Support this test for the platform backend once the global search API is
-//  public.
-public class GlobalSearchSessionLocalCtsTest extends GlobalSearchSessionCtsTestBase {
+public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase{
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
-    }
-
-    @Override
-    protected ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
-        Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createGlobalSearchSession(
-                new LocalStorage.GlobalSearchContext.Builder(context).build());
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java
new file mode 100644
index 0000000..405d82e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.cts.app;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class AppSearchSchemaMigrationPlatformCtsTest extends AppSearchSchemaMigrationCtsTestBase{
+    @Override
+    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+        Context context = ApplicationProvider.getApplicationContext();
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName).build());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
new file mode 100644
index 0000000..6fb1313
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -0,0 +1,3044 @@
+/*
+ * 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 androidx.appsearch.cts.app;
+
+import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
+import static androidx.appsearch.app.AppSearchResult.RESULT_NOT_FOUND;
+import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
+import static androidx.appsearch.app.util.AppSearchTestUtils.doGet;
+import static androidx.appsearch.app.util.AppSearchTestUtils.retrieveAllSearchResults;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.util.AppSearchEmail;
+import androidx.appsearch.cts.app.customer.EmailDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public abstract class AppSearchSessionCtsTestBase {
+    private static final String DB_NAME_1 = "";
+    private static final String DB_NAME_2 = "testDb2";
+
+    private AppSearchSession mDb1;
+    private AppSearchSession mDb2;
+
+    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull String dbName);
+
+    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull String dbName, @NonNull ExecutorService executor);
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        mDb2 = createSearchSession(DB_NAME_2).get();
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        cleanup();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        cleanup();
+    }
+
+    private void cleanup() throws Exception {
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    @Test
+    public void testSetSchema() throws Exception {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+    }
+
+    @Test
+    public void testSetSchema_Failure() throws Exception {
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        AppSearchSchema emailSchema1 = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                .build();
+
+        Throwable throwable = assertThrows(ExecutionException.class,
+                () -> mDb1.setSchema(new SetSchemaRequest.Builder()
+                        .addSchemas(emailSchema1).build()).get()).getCause();
+        assertThat(throwable).isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) throwable;
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+        assertThat(exception).hasMessageThat().contains("Incompatible types: {builtin:Email}");
+
+        throwable = assertThrows(ExecutionException.class,
+                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+
+        assertThat(throwable).isInstanceOf(AppSearchException.class);
+        exception = (AppSearchException) throwable;
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+        assertThat(exception).hasMessageThat().contains("Deleted types: {builtin:Email}");
+    }
+
+    @Test
+    public void testSetSchema_updateVersion() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(1).build()).get();
+
+        Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchema().get().getSchemas();
+        assertThat(actualSchemaTypes).containsExactly(schema);
+
+        // increase version number
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(2).build()).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(2);
+    }
+
+    @Test
+    public void testSetSchema_checkVersion() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // set different version number to different database.
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(135).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(246).build()).get();
+
+
+        // check the version has been set correctly.
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(135);
+
+        getSchemaResponse = mDb2.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(246);
+    }
+
+// @exportToFramework:startStrip()
+
+    @Test
+    public void testSetSchema_addDocumentClasses() throws Exception {
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailDocument.class).build()).get();
+    }
+// @exportToFramework:endStrip()
+
+// @exportToFramework:startStrip()
+
+    @Test
+    public void testGetSchema() throws Exception {
+        AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest request1 = new SetSchemaRequest.Builder()
+                .addSchemas(emailSchema1).addDocumentClasses(EmailDocument.class).build();
+        SetSchemaRequest request2 = new SetSchemaRequest.Builder()
+                .addSchemas(emailSchema2).addDocumentClasses(EmailDocument.class).build();
+
+        mDb1.setSchema(request1).get();
+        mDb2.setSchema(request2).get();
+
+        Set<AppSearchSchema> actual1 = mDb1.getSchema().get().getSchemas();
+        assertThat(actual1).hasSize(2);
+        assertThat(actual1).isEqualTo(request1.getSchemas());
+        Set<AppSearchSchema> actual2 = mDb2.getSchema().get().getSchemas();
+        assertThat(actual2).hasSize(2);
+        assertThat(actual2).isEqualTo(request2.getSchemas());
+    }
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testGetSchema_allPropertyTypes() throws Exception {
+        AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
+                .addProperty(new StringPropertyConfig.Builder("string")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        "document", AppSearchEmail.SCHEMA_TYPE)
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+
+        // Add it to AppSearch and then obtain it again
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(inSchema, AppSearchEmail.SCHEMA).build()).get();
+        GetSchemaResponse response = mDb1.getSchema().get();
+        List<AppSearchSchema> schemas = new ArrayList<>(response.getSchemas());
+        assertThat(schemas).containsExactly(inSchema, AppSearchEmail.SCHEMA);
+        AppSearchSchema outSchema;
+        if (schemas.get(0).getSchemaType().equals("Test")) {
+            outSchema = schemas.get(0);
+        } else {
+            outSchema = schemas.get(1);
+        }
+        assertThat(outSchema.getSchemaType()).isEqualTo("Test");
+        assertThat(outSchema).isNotSameInstanceAs(inSchema);
+
+        List<PropertyConfig> properties = outSchema.getProperties();
+        assertThat(properties).hasSize(6);
+
+        assertThat(properties.get(0).getName()).isEqualTo("string");
+        assertThat(properties.get(0).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(((StringPropertyConfig) properties.get(0)).getIndexingType())
+                .isEqualTo(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+        assertThat(((StringPropertyConfig) properties.get(0)).getTokenizerType())
+                .isEqualTo(StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
+
+        assertThat(properties.get(1).getName()).isEqualTo("long");
+        assertThat(properties.get(1).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(properties.get(1)).isInstanceOf(AppSearchSchema.LongPropertyConfig.class);
+
+        assertThat(properties.get(2).getName()).isEqualTo("double");
+        assertThat(properties.get(2).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(properties.get(2)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
+
+        assertThat(properties.get(3).getName()).isEqualTo("boolean");
+        assertThat(properties.get(3).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
+
+        assertThat(properties.get(4).getName()).isEqualTo("bytes");
+        assertThat(properties.get(4).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
+
+        assertThat(properties.get(5).getName()).isEqualTo("document");
+        assertThat(properties.get(5).getCardinality())
+                .isEqualTo(PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5)).getSchemaType())
+                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(5))
+                .shouldIndexNestedProperties()).isEqualTo(true);
+    }
+
+    @Test
+    public void testGetNamespaces() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        assertThat(mDb1.getNamespaces().get()).isEmpty();
+
+        // Index a document
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace1", "id1").build())
+                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1");
+
+        // Index additional data
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(
+                        new AppSearchEmail.Builder("namespace2", "id1").build(),
+                        new AppSearchEmail.Builder("namespace2", "id2").build(),
+                        new AppSearchEmail.Builder("namespace3", "id1").build())
+                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly(
+                "namespace1", "namespace2", "namespace3");
+
+        // Remove namespace2/id2 -- namespace2 should still exist because of namespace2/id1
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+                        "id2").build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly(
+                "namespace1", "namespace2", "namespace3");
+
+        // Remove namespace2/id1 -- namespace2 should now be gone
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+                        "id1").build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+
+        // Make sure the list of namespaces is preserved after restart
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+    }
+
+    @Test
+    public void testGetNamespaces_dbIsolation() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        assertThat(mDb1.getNamespaces().get()).isEmpty();
+        assertThat(mDb2.getNamespaces().get()).isEmpty();
+
+        // Index documents
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace1_db1", "id1").build())
+                .build()));
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace2_db1", "id1").build())
+                .build()));
+        checkIsBatchResultSuccess(mDb2.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace_db2", "id1").build())
+                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+
+        // Make sure the list of namespaces is preserved after restart
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+    }
+
+    @Test
+    public void testGetSchema_emptyDB() throws Exception {
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(0);
+    }
+
+    @Test
+    public void testPutDocuments() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+                .setFrom("from@example.com")
+                .setTo("to1@example.com", "to2@example.com")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @Test
+    public void testPutLargeDocument() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        char[] chars = new char[16_000_000];
+        Arrays.fill(chars, ' ');
+        String body = new StringBuilder().append(chars).append("the end.").toString();
+
+        // Index a document
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+                .setFrom("from@example.com")
+                .setTo("to1@example.com", "to2@example.com")
+                .setSubject("testPut example")
+                .setBody(body)
+                .build();
+        AppSearchBatchResult<String, Void> result = mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
+        assertThat(result.isSuccess()).isTrue();
+
+        SearchResults searchResults = mDb1.search("end", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> outDocuments = convertSearchResultsToDocuments(searchResults);
+        assertThat(outDocuments).hasSize(1);
+        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email);
+    }
+
+    @Test
+    public void testPutLargeDocument_exceedLimit() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Create a String property that make the document exceed the total size limit.
+        char[] chars = new char[17_000_000];
+        String body = new StringBuilder().append(chars).toString();
+
+        // Index a document
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+                .setFrom("from@example.com")
+                .setTo("to1@example.com", "to2@example.com")
+                .setSubject("testPut example")
+                .setBody(body)
+                .build();
+        AppSearchBatchResult<String, Void> result = mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
+        assertThat(result.getFailures()).containsKey("id1");
+        assertThat(result.getFailures().get("id1").getErrorMessage())
+                .contains("was too large to write. Max is 16777215");
+    }
+
+// @exportToFramework:startStrip()
+
+    @Test
+    public void testPut_addDocumentClasses() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailDocument.class).build()).get();
+
+        // Index a document
+        EmailDocument email = new EmailDocument();
+        email.namespace = "namespace";
+        email.id = "id1";
+        email.subject = "testPut example";
+        email.body = "This is the body of the testPut email";
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addDocuments(email).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testUpdateSchema() throws Exception {
+        // Schema registration
+        AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema newEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build()).get();
+
+        // Try to index a gift. This should fail as it's not in the schema.
+        GenericDocument gift =
+                new GenericDocument.Builder<>("namespace", "gift1", "Gift").setPropertyLong("price",
+                        5).build();
+        AppSearchBatchResult<String, Void> result =
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()).get();
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getFailures().get("gift1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Update the schema to include the gift and update email with a new field
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(newEmailSchema, giftSchema).build()).get();
+
+        // Try to index the document again, which should now work
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()));
+
+        // Indexing an email with a body should also work
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+    }
+
+    @Test
+    public void testRemoveSchema() throws Exception {
+        // Schema registration
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+
+        // Index an email and check it present.
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1")
+                .setSubject("testPut example")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        List<GenericDocument> outDocuments =
+                doGet(mDb1, "namespace", "email1");
+        assertThat(outDocuments).hasSize(1);
+        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email);
+
+        // Try to remove the email schema. This should fail as it's an incompatible change.
+        Throwable failResult1 = assertThrows(
+                ExecutionException.class,
+                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+        assertThat(failResult1).isInstanceOf(AppSearchException.class);
+        assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
+        assertThat(failResult1).hasMessageThat().contains(
+                "Deleted types: {builtin:Email}");
+
+        // Try to remove the email schema again, which should now work as we set forceOverride to
+        // be true.
+        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+
+        // Make sure the indexed email is gone.
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds("email1")
+                        .build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("email1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Try to index an email again. This should fail as the schema has been removed.
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
+                .setSubject("testPut example")
+                .build();
+        AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()).get();
+        assertThat(failResult2.isSuccess()).isFalse();
+        assertThat(failResult2.getFailures().get("email2").getErrorMessage())
+                .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
+                        + "/builtin:Email' not found");
+    }
+
+    @Test
+    public void testRemoveSchema_twoDatabases() throws Exception {
+        // Schema registration in mDb1 and mDb2
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+
+        // Index an email and check it present in database1.
+        AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "email1")
+                .setSubject("testPut example")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        List<GenericDocument> outDocuments =
+                doGet(mDb1, "namespace", "email1");
+        assertThat(outDocuments).hasSize(1);
+        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email1);
+
+        // Index an email and check it present in database2.
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
+                .setSubject("testPut example")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb2.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+        outDocuments = doGet(mDb2, "namespace", "email2");
+        assertThat(outDocuments).hasSize(1);
+        outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email2);
+
+        // Try to remove the email schema in database1. This should fail as it's an incompatible
+        // change.
+        Throwable failResult1 = assertThrows(
+                ExecutionException.class,
+                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+        assertThat(failResult1).isInstanceOf(AppSearchException.class);
+        assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
+        assertThat(failResult1).hasMessageThat().contains(
+                "Deleted types: {builtin:Email}");
+
+        // Try to remove the email schema again, which should now work as we set forceOverride to
+        // be true.
+        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+
+        // Make sure the indexed email is gone in database 1.
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds("email1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("email1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Try to index an email again. This should fail as the schema has been removed.
+        AppSearchEmail email3 = new AppSearchEmail.Builder("namespace", "email3")
+                .setSubject("testPut example")
+                .build();
+        AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()).get();
+        assertThat(failResult2.isSuccess()).isFalse();
+        assertThat(failResult2.getFailures().get("email3").getErrorMessage())
+                .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
+                        + "/builtin:Email' not found");
+
+        // Make sure email in database 2 still present.
+        outDocuments = doGet(mDb2, "namespace", "email2");
+        assertThat(outDocuments).hasSize(1);
+        outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(email2);
+
+        // Make sure email could still be indexed in database 2.
+        checkIsBatchResultSuccess(
+                mDb2.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+    }
+
+    @Test
+    public void testGetDocuments() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Get the document
+        List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1");
+        assertThat(outDocuments).hasSize(1);
+        AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
+        assertThat(outEmail).isEqualTo(inEmail);
+
+        // Can't get the document in the other instance.
+        AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()).get();
+        assertThat(failResult.isSuccess()).isFalse();
+        assertThat(failResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+// @exportToFramework:startStrip()
+
+    @Test
+    public void testGet_addDocumentClasses() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailDocument.class).build()).get();
+
+        // Index a document
+        EmailDocument inEmail = new EmailDocument();
+        inEmail.namespace = "namespace";
+        inEmail.id = "id1";
+        inEmail.subject = "testPut example";
+        inEmail.body = "This is the body of the testPut inEmail";
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addDocuments(inEmail).build()));
+
+        // Get the document
+        List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1");
+        assertThat(outDocuments).hasSize(1);
+        EmailDocument outEmail = outDocuments.get(0).toDocumentClass(EmailDocument.class);
+        assertThat(inEmail.id).isEqualTo(outEmail.id);
+        assertThat(inEmail.subject).isEqualTo(outEmail.subject);
+        assertThat(inEmail.body).isEqualTo(outEmail.body);
+    }
+// @exportToFramework:endStrip()
+
+
+    @Test
+    public void testGetDocuments_projection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection(
+                        AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_projectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds(
+                "id1",
+                "id2").addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList()).build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_projectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection("NonExistentType", Collections.emptyList())
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection(
+                        GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds(
+                "id1",
+                "id2").addProjection(GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                Collections.emptyList()).build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection("NonExistentType", Collections.emptyList())
+                .addProjection(
+                        GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testQuery() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents.get(0)).isEqualTo(inEmail);
+
+        // Multi-term query
+        searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents.get(0)).isEqualTo(inEmail);
+    }
+
+    @Test
+    public void testQuery_getNextPage() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        Set<AppSearchEmail> emailSet = new HashSet<>();
+        PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
+        // Index 31 documents
+        for (int i = 0; i < 31; i++) {
+            AppSearchEmail inEmail =
+                    new AppSearchEmail.Builder("namespace", "id" + i)
+                            .setFrom("from@example.com")
+                            .setTo("to1@example.com", "to2@example.com")
+                            .setSubject("testPut example")
+                            .setBody("This is the body of the testPut email")
+                            .build();
+            emailSet.add(inEmail);
+            putDocumentsRequestBuilder.addGenericDocuments(inEmail);
+        }
+        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+
+        // Set number of results per page is 7.
+        SearchResults searchResults = mDb1.search("body",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultCountPerPage(7)
+                        .build());
+        List<GenericDocument> documents = new ArrayList<>();
+
+        int pageNumber = 0;
+        List<SearchResult> results;
+
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            ++pageNumber;
+            for (SearchResult result : results) {
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+
+        // check all document presents
+        assertThat(documents).containsExactlyElementsIn(emailSet);
+        assertThat(pageNumber).isEqualTo(6); // 5 (upper(31/7)) + 1 (final empty page)
+    }
+
+    @Test
+    public void testQuery_relevanceScoring() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("Mary had a little lamb")
+                        .setBody("A little lamb, little lamb")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("I'm a little teapot")
+                        .setBody("short and stout. Here is my handle, here is my spout.")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Query for "little". It should match both emails.
+        SearchResults searchResults = mDb1.search("little", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .build());
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+
+        // The email1 should be ranked higher because 'little' appears three times in email1 and
+        // only once in email2.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(
+                results.get(1).getRankingSignal());
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
+
+        // Query for "little OR stout". It should match both emails.
+        searchResults = mDb1.search("little OR stout", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .build());
+        results = retrieveAllSearchResults(searchResults);
+
+        // The email2 should be ranked higher because 'little' appears once and "stout", which is a
+        // rarer term, appears once. email1 only has the three 'little' appearances.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(
+                results.get(1).getRankingSignal());
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
+    }
+
+    @Test
+    public void testQuery_typeFilter() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
+                .addProperty(new StringPropertyConfig.Builder("foo")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build()
+                ).build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(genericSchema)
+                        .build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id2", "Generic")
+                .setPropertyString("foo", "body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inDoc).build()));
+
+        // Query for the documents
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(inEmail, inDoc);
+
+        // Query only for Document
+        searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .addFilterSchemas("Generic", "Generic") // duplicate type in filter won't matter.
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(inDoc);
+    }
+
+    @Test
+    public void testQuery_packageFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Query for the document within our package
+        SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterPackageNames(ApplicationProvider.getApplicationContext().getPackageName())
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(email);
+
+        // Query for the document in some other package, which won't exist
+        searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterPackageNames("some.other.package")
+                .build());
+        List<SearchResult> results = searchResults.getNextPage().get();
+        assertThat(results).isEmpty();
+    }
+
+    @Test
+    public void testQuery_namespaceFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index two documents
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("expectedNamespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail unexpectedEmail =
+                new AppSearchEmail.Builder("unexpectedNamespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(expectedEmail, unexpectedEmail).build()));
+
+        // Query for all namespaces
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        assertThat(documents).containsExactly(expectedEmail, unexpectedEmail);
+
+        // Query only for expectedNamespace
+        searchResults = mDb1.search("body",
+                new SearchSpec.Builder()
+                        .addFilterNamespaces("expectedNamespace")
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(expectedEmail);
+    }
+
+    @Test
+    public void testQuery_getPackageName() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        List<SearchResult> results;
+        List<GenericDocument> documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getPackageName()).isEqualTo(
+                        ApplicationProvider.getApplicationContext().getPackageName());
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+    }
+
+    @Test
+    public void testQuery_getDatabaseName() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        List<SearchResult> results;
+        List<GenericDocument> documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1);
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+
+        // Schema registration for another database
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        searchResults = mDb2.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2);
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+    }
+
+    @Test
+    public void testQuery_projection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"Email", ["body", "to"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to"
+        // properties. The note document should have been returned with all of its properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_projectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"Email", []}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned without any properties. The note document
+        // should have been returned with all of its properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_projectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection("NonExistentType", Collections.emptyList())
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to" properties.
+        // The note document should have been returned with all of its properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_wildcardProjection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*", ["body", "to"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection(
+                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to"
+        // properties. The note document should have been returned with only the "body" property.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("body", "Note body").build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_wildcardProjectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+                                .build()).build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*", []}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email and note documents should have been returned without any properties.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000).build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_wildcardProjectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection("NonExistentType", Collections.emptyList())
+                .addProjection(
+                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The email document should have been returned with only the "body" and "to"
+        // properties. The note document should have been returned with only the "body" property.
+        AppSearchEmail expectedEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument expectedNote =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("body", "Note body").build();
+        assertThat(documents).containsExactly(expectedNote, expectedEmail);
+    }
+
+    @Test
+    public void testQuery_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document to instance 1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+
+        // Index a document to instance 2.
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Query for instance 1.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(inEmail1);
+
+        // Query for instance 2.
+        searchResults = mDb2.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(1);
+        assertThat(documents).containsExactly(inEmail2);
+    }
+
+    @Test
+    public void testSnippet() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build()
+                ).build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
+
+        // Index a document
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "Generic")
+                        .setPropertyString("subject", "A commonly used fake word is foo. "
+                                + "Another nonsense word that’s used a lot is bar")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search("foo",
+                new SearchSpec.Builder()
+                        .addFilterSchemas("Generic")
+                        .setSnippetCount(1)
+                        .setSnippetCountPerProperty(1)
+                        .setMaxSnippetSize(10)
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .build());
+        List<SearchResult> results = searchResults.getNextPage().get();
+        assertThat(results).hasSize(1);
+
+        List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
+        assertThat(matchInfos).isNotNull();
+        assertThat(matchInfos).hasSize(1);
+        SearchResult.MatchInfo matchInfo = matchInfos.get(0);
+        assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
+                + "Another nonsense word that’s used a lot is bar");
+        assertThat(matchInfo.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/29,  /*upper=*/32));
+        assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
+        assertThat(matchInfo.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/26,  /*upper=*/33));
+        assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
+    }
+
+    @Test
+    public void testSetSnippetCount() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build()
+                ).build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
+
+        // Index documents
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(
+                new GenericDocument.Builder<>("namespace", "id1", "Generic")
+                        .setPropertyString(
+                                "subject",
+                                "I like cats", "I like dogs", "I like birds", "I like fish")
+                        .setScore(10)
+                        .build(),
+                new GenericDocument.Builder<>("namespace", "id2", "Generic")
+                        .setPropertyString(
+                                "subject",
+                                "I like red", "I like green", "I like blue", "I like yellow")
+                        .setScore(20)
+                        .build(),
+                new GenericDocument.Builder<>("namespace", "id3", "Generic")
+                        .setPropertyString(
+                                "subject",
+                                "I like cupcakes",
+                                "I like donuts",
+                                "I like eclairs",
+                                "I like froyo")
+                        .setScore(5)
+                        .build())
+                .build()));
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search(
+                "like",
+                new SearchSpec.Builder()
+                        .addFilterSchemas("Generic")
+                        .setSnippetCount(2)
+                        .setSnippetCountPerProperty(3)
+                        .setMaxSnippetSize(11)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .build());
+
+        // Check result 1
+        List<SearchResult> results = searchResults.getNextPage().get();
+        assertThat(results).hasSize(3);
+
+        assertThat(results.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
+        assertThat(matchInfos).hasSize(3);
+        assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like red");
+        assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like");
+        assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like blue");
+
+        // Check result 2
+        assertThat(results.get(1).getGenericDocument().getId()).isEqualTo("id1");
+        matchInfos = results.get(1).getMatchInfos();
+        assertThat(matchInfos).hasSize(3);
+        assertThat(matchInfos.get(0).getSnippet()).isEqualTo("I like cats");
+        assertThat(matchInfos.get(1).getSnippet()).isEqualTo("I like dogs");
+        assertThat(matchInfos.get(2).getSnippet()).isEqualTo("I like");
+
+        // Check result 2
+        assertThat(results.get(2).getGenericDocument().getId()).isEqualTo("id3");
+        matchInfos = results.get(2).getMatchInfos();
+        assertThat(matchInfos).isEmpty();
+    }
+
+    @Test
+    public void testCJKSnippet() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build()
+                ).build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
+
+        String japanese =
+                "差し出されたのが今日ランドセルでした普通の子であれば満面の笑みで俺を言うでしょうしかし私は赤いランド"
+                + "セルを見て笑うことができませんでしたどうしたのと心配そうな仕事ガラスながら渋い顔する私書いたこと言"
+                + "うんじゃないのカードとなる声を聞きたい私は目から涙をこぼしながらおじいちゃんの近くにかけおり頭をポ"
+                + "ンポンと叩きピンクが良かったんだもん";
+        // Index a document
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "Generic")
+                        .setPropertyString("subject", japanese)
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search("は",
+                new SearchSpec.Builder()
+                        .addFilterSchemas("Generic")
+                        .setSnippetCount(1)
+                        .setSnippetCountPerProperty(1)
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .build());
+        List<SearchResult> results = searchResults.getNextPage().get();
+        assertThat(results).hasSize(1);
+
+        List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
+        assertThat(matchInfos).isNotNull();
+        assertThat(matchInfos).hasSize(1);
+        SearchResult.MatchInfo matchInfo = matchInfos.get(0);
+        assertThat(matchInfo.getFullText()).isEqualTo(japanese);
+        assertThat(matchInfo.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/44,  /*upper=*/45));
+        assertThat(matchInfo.getExactMatch()).isEqualTo("は");
+    }
+
+    @Test
+    public void testRemove() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
+
+        // Delete the document
+        checkIsBatchResultSuccess(mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()));
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
+                        "id2").build())
+                .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
+
+        // Test if we delete a nonexistent id.
+        AppSearchBatchResult<String, Void> deleteResult = mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()).get();
+
+        assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
+                AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemove_multipleIds() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
+
+        // Delete the document
+        checkIsBatchResultSuccess(mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build()));
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
+                        "id2").build())
+                .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getFailures().get("id2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemoveByQuery() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("bar")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
+
+        // Delete the email 1 by query "foo"
+        mDb1.remove("foo",
+                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
+                .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
+
+        // Delete the email 2 by query "bar"
+        mDb1.remove("bar",
+                new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
+        getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build())
+                .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemoveByQuery_packageFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+
+        // Try to delete email with query "foo", but restricted to a different package name.
+        // Won't work and email will still exist.
+        mDb1.remove("foo",
+                new SearchSpec.Builder().setTermMatch(
+                        SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
+                        "some.other.package").build()).get();
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+
+        // Delete the email by query "foo", restricted to the correct package this time.
+        mDb1.remove("foo", new SearchSpec.Builder().setTermMatch(
+                SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
+                ApplicationProvider.getApplicationContext().getPackageName()).build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
+                .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemove_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+
+        // Can't delete in the other instance.
+        AppSearchBatchResult<String, Void> deleteResult = mDb2.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
+                AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+
+        // Delete the document
+        checkIsBatchResultSuccess(mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Test if we delete a nonexistent id.
+        deleteResult = mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
+                AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemoveByTypes() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
+                        genericSchema).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "id3", "Generic").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
+                        .build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3);
+
+        // Delete the email type
+        mDb1.remove("",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                        .build())
+                .get();
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2", "id3").build())
+                .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getFailures().get("id2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
+    }
+
+    @Test
+    public void testRemoveByTypes_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
+
+        // Delete the email type in instance 1
+        mDb1.remove("",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                        .build())
+                .get();
+
+        // Make sure it's really gone in instance 1
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Make sure it's still in instance 2.
+        getResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
+    }
+
+    @Test
+    public void testRemoveByNamespace() throws Exception {
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
+                .addProperty(new StringPropertyConfig.Builder("foo")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build()
+                ).build();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
+                        genericSchema).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("email", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("email", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("document", "id3", "Generic")
+                        .setPropertyString("foo", "bar").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
+                        .build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, /*namespace=*/"email", "id1", "id2")).hasSize(2);
+        assertThat(doGet(mDb1, /*namespace=*/"document", "id3")).hasSize(1);
+
+        // Delete the email namespace
+        mDb1.remove("",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .addFilterNamespaces("email")
+                        .build())
+                .get();
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("email")
+                        .addIds("id1", "id2").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(getResult.getFailures().get("id2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("document")
+                        .addIds("id3").build()).get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
+    }
+
+    @Test
+    public void testRemoveByNamespaces_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("email", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("email", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, /*namespace=*/"email", "id1")).hasSize(1);
+        assertThat(doGet(mDb2, /*namespace=*/"email", "id2")).hasSize(1);
+
+        // Delete the email namespace in instance 1
+        mDb1.remove("",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .addFilterNamespaces("email")
+                        .build())
+                .get();
+
+        // Make sure it's really gone in instance 1
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("email")
+                        .addIds("id1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Make sure it's still in instance 2.
+        getResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("email")
+                        .addIds("id2").build()).get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
+    }
+
+    @Test
+    public void testRemoveAll_twoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
+
+        // Delete the all document in instance 1
+        mDb1.remove("",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .build())
+                .get();
+
+        // Make sure it's really gone in instance 1
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Make sure it's still in instance 2.
+        getResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
+        assertThat(getResult.isSuccess()).isTrue();
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
+    }
+
+    @Test
+    public void testRemoveAll_termMatchType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 2")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        AppSearchEmail email3 =
+                new AppSearchEmail.Builder("namespace", "id3")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 3")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        AppSearchEmail email4 =
+                new AppSearchEmail.Builder("namespace", "id4")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example 4")
+                        .setBody("This is the body of the testPut second email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email3, email4).build()));
+
+        // Check the presence of the documents
+        SearchResults searchResults = mDb1.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+        searchResults = mDb2.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).hasSize(2);
+
+        // Delete the all document in instance 1 with TERM_MATCH_PREFIX
+        mDb1.remove("",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                        .build())
+                .get();
+        searchResults = mDb1.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).isEmpty();
+
+        // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
+        mDb2.remove("",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build())
+                .get();
+        searchResults = mDb2.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).isEmpty();
+    }
+
+    @Test
+    public void testRemoveAllAfterEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+
+        // Remove the document
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()));
+
+        // Make sure it's really gone
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // Delete the all documents
+        mDb1.remove(
+                "", new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
+                .get();
+
+        // Make sure it's still gone
+        getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testCloseAndReopen() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // close and re-open the appSearchSession
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail);
+    }
+
+    @Test
+    public void testCallAfterClose() throws Exception {
+
+        // Create a same-thread database by inject an executor which could help us maintain the
+        // execution order of those async tasks.
+        Context context = ApplicationProvider.getApplicationContext();
+        AppSearchSession sameThreadDb = createSearchSession(
+                "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
+
+        try {
+            // Schema registration -- just mutate something
+            sameThreadDb.setSchema(
+                    new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+            // Close the database. No further call will be allowed.
+            sameThreadDb.close();
+
+            // Try to query the closed database
+            // We are using the same-thread db here to make sure it has been closed.
+            IllegalStateException e = assertThrows(IllegalStateException.class, () ->
+                    sameThreadDb.search("query", new SearchSpec.Builder()
+                            .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                            .build()));
+            assertThat(e).hasMessageThat().contains("SearchSession has already been closed");
+        } finally {
+            // To clean the data that has been added in the test, need to re-open the session and
+            // set an empty schema.
+            AppSearchSession reopen = createSearchSession(
+                    "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
+            reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        }
+    }
+
+    @Test
+    public void testReportUsage() throws Exception {
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index two documents.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1").build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
+
+        // Email 1 has more usages, but email 2 has more recent usages.
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+                .setUsageTimestampMillis(1000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+                .setUsageTimestampMillis(2000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+                .setUsageTimestampMillis(3000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+                .setUsageTimestampMillis(10000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+                .setUsageTimestampMillis(20000).build()).get();
+
+        // Query by number of usages
+        List<SearchResult> results = retrieveAllSearchResults(
+                mDb1.search("", new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build()));
+        // Email 1 has three usages and email 2 has two usages.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
+        assertThat(results.get(1).getRankingSignal()).isEqualTo(2);
+
+        // Query by most recent usage.
+        results = retrieveAllSearchResults(
+                mDb1.search("", new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build()));
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(0).getRankingSignal()).isEqualTo(20000);
+        assertThat(results.get(1).getRankingSignal()).isEqualTo(3000);
+    }
+
+    @Test
+    public void testReportUsage_invalidNamespace() throws Exception {
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Use the correct namespace; it works
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
+
+        // Use an incorrect namespace; it fails
+        ExecutionException e = assertThrows(
+                ExecutionException.class,
+                () -> mDb1.reportUsage(
+                        new ReportUsageRequest.Builder("namespace2", "id1").build()).get());
+        assertThat(e).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException cause = (AppSearchException) e.getCause();
+        assertThat(cause.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testGetStorageInfo() throws Exception {
+        StorageInfo storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Still no storage space attributed with just a schema
+        storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+
+        // Index two documents.
+        AppSearchEmail email1 = new AppSearchEmail.Builder("namespace1", "id1").build();
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace1", "id2").build();
+        AppSearchEmail email3 = new AppSearchEmail.Builder("namespace2", "id1").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2,
+                        email3).build()));
+
+        // Non-zero size now
+        storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void testFlush() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+                .setFrom("from@example.com")
+                .setTo("to1@example.com", "to2@example.com")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // The future returned from requestFlush will be set as a void or an Exception on error.
+        mDb1.requestFlush().get();
+    }
+
+    @Test
+    public void testQuery_ResultGroupingLimits() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index four documents.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace1", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace2", "id3")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // Query with per package result grouping. Only the last document 'email4' should be
+        // returned.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('email4' and 'email2').
+        searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4, inEmail2);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('email4' and 'email2').
+        searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4, inEmail2);
+    }
+
+    @Test
+    public void testIndexNestedDocuments() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA)
+                .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "prop", AppSearchEmail.SCHEMA_TYPE)
+                                .setShouldIndexNestedProperties(true)
+                                .build())
+                        .build())
+                .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "prop", AppSearchEmail.SCHEMA_TYPE)
+                                .setShouldIndexNestedProperties(false)
+                                .build())
+                        .build())
+                .build())
+                .get();
+
+        // Index the documents.
+        AppSearchEmail email = new AppSearchEmail.Builder("", "")
+                .setSubject("This is the body")
+                .build();
+        GenericDocument yesNestedIndex =
+                new GenericDocument.Builder<>("namespace", "yesNestedIndex", "YesNestedIndex")
+                        .setPropertyDocument("prop", email)
+                        .build();
+        GenericDocument noNestedIndex =
+                new GenericDocument.Builder<>("namespace", "noNestedIndex", "NoNestedIndex")
+                        .setPropertyDocument("prop", email)
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(yesNestedIndex, noNestedIndex).build()));
+
+        // Query.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setSnippetCount(10)
+                .setSnippetCountPerProperty(10)
+                .build());
+        List<SearchResult> page = searchResults.getNextPage().get();
+        assertThat(page).hasSize(1);
+        assertThat(page.get(0).getGenericDocument()).isEqualTo(yesNestedIndex);
+        List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
+        assertThat(matches).hasSize(1);
+        assertThat(matches.get(0).getPropertyPath()).isEqualTo("prop.subject");
+        assertThat(matches.get(0).getFullText()).isEqualTo("This is the body");
+        assertThat(matches.get(0).getExactMatch()).isEqualTo("body");
+    }
+
+    @Test
+    public void testCJKTQuery() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document to instance 1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "uri1")
+                        .setBody("他是個男孩 is a boy")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+
+        // Query for "他" (He)
+        SearchResults searchResults = mDb1.search("他", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail1);
+
+        // Query for "男孩" (boy)
+        searchResults = mDb1.search("男孩", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail1);
+
+        // Query for "boy"
+        searchResults = mDb1.search("boy", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail1);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
similarity index 85%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
index 541cd5a..9462f55 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 // @exportToFramework:skipFile()
-package androidx.appsearch.app.cts;
+package androidx.appsearch.cts.app;
 
 import android.content.Context;
 
@@ -32,7 +32,7 @@
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
@@ -40,7 +40,7 @@
             @NonNull String dbName, @NonNull ExecutorService executor) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build(),
-                executor);
+                new LocalStorage.SearchContext.Builder(context, dbName)
+                        .setWorkerExecutor(executor).build());
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
similarity index 68%
copy from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java
copy to appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
index 541cd5a..f467c77 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
@@ -14,33 +14,36 @@
  * limitations under the License.
  */
 // @exportToFramework:skipFile()
-package androidx.appsearch.app.cts;
+package androidx.appsearch.cts.app;
 
 import android.content.Context;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.localstorage.LocalStorage;
+import androidx.appsearch.platformstorage.PlatformStorage;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.concurrent.ExecutorService;
 
-public class AppSearchSessionLocalCtsTest extends AppSearchSessionCtsTestBase {
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class AppSearchSessionPlatformCtsTest extends AppSearchSessionCtsTestBase {
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSession(
             @NonNull String dbName, @NonNull ExecutorService executor) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build(),
-                executor);
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName)
+                        .setWorkerExecutor(executor).build());
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
new file mode 100644
index 0000000..ce25fef
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
@@ -0,0 +1,854 @@
+/*
+ * 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+
+import org.junit.Test;
+
+public class GenericDocumentCtsTest {
+    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, (byte) 7};
+    private static final GenericDocument sDocumentProperties1 = new GenericDocument
+            .Builder<>("namespace", "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+            .setCreationTimestampMillis(12345L)
+            .build();
+    private static final GenericDocument sDocumentProperties2 = new GenericDocument
+            .Builder<>("namespace", "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+            .setCreationTimestampMillis(6789L)
+            .build();
+
+    @Test
+    public void testMaxIndexedProperties() {
+        assertThat(GenericDocument.getMaxIndexedProperties()).isEqualTo(16);
+    }
+
+    @Test
+    public void testDocumentEquals_identical() {
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_differentOrder() {
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        // Create second document with same parameter but different order.
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_failure() {
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .build();
+
+        // Create second document with same order but different value.
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 4L) // Different
+                .build();
+        assertThat(document1).isNotEqualTo(document2);
+        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentEquals_repeatedFieldOrder_failure() {
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .build();
+
+        // Create second document with same order but different value.
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyBoolean("booleanKey1", true, true, false) // Different
+                .build();
+        assertThat(document1).isNotEqualTo(document2);
+        assertThat(document1.hashCode()).isNotEqualTo(document2.hashCode());
+    }
+
+    @Test
+    public void testDocumentGetSingleValue() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L)
+                .setPropertyDouble("doubleKey1", 1.0)
+                .setPropertyBoolean("booleanKey1", true)
+                .setPropertyString("stringKey1", "test-value1")
+                .setPropertyBytes("byteKey1", sByteArray1)
+                .setPropertyDocument("documentKey1", sDocumentProperties1)
+                .build();
+        assertThat(document.getId()).isEqualTo("id1");
+        assertThat(document.getTtlMillis()).isEqualTo(1L);
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
+        assertThat(document.getScore()).isEqualTo(1);
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
+        assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+        assertThat(document.getPropertyBytes("byteKey1"))
+                .asList().containsExactly((byte) 1, (byte) 2, (byte) 3).inOrder();
+        assertThat(document.getPropertyDocument("documentKey1")).isEqualTo(sDocumentProperties1);
+
+        assertThat(document.getProperty("longKey1")).isInstanceOf(long[].class);
+        assertThat((long[]) document.getProperty("longKey1")).asList().containsExactly(1L);
+        assertThat(document.getProperty("doubleKey1")).isInstanceOf(double[].class);
+        assertThat((double[]) document.getProperty("doubleKey1")).usingTolerance(
+                0.05).containsExactly(1.0);
+        assertThat(document.getProperty("booleanKey1")).isInstanceOf(boolean[].class);
+        assertThat((boolean[]) document.getProperty("booleanKey1")).asList().containsExactly(true);
+        assertThat(document.getProperty("stringKey1")).isInstanceOf(String[].class);
+        assertThat((String[]) document.getProperty("stringKey1")).asList().containsExactly(
+                "test-value1");
+        assertThat(document.getProperty("byteKey1")).isInstanceOf(byte[][].class);
+        assertThat((byte[][]) document.getProperty("byteKey1")).asList().containsExactly(
+                sByteArray1).inOrder();
+        assertThat(document.getProperty("documentKey1")).isInstanceOf(GenericDocument[].class);
+        assertThat(
+                (GenericDocument[]) document.getProperty("documentKey1")).asList().containsExactly(
+                sDocumentProperties1);
+    }
+
+    @Test
+    public void testDocumentGetArrayValues() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .build();
+
+        assertThat(document.getId()).isEqualTo("id1");
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L).inOrder();
+        assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
+                .containsExactly(1.0, 2.0, 3.0).inOrder();
+        assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true).inOrder();
+        assertThat(document.getPropertyStringArray("stringKey1")).asList()
+                .containsExactly("test-value1", "test-value2", "test-value3").inOrder();
+        assertThat(document.getPropertyBytesArray("byteKey1")).asList()
+                .containsExactly(sByteArray1, sByteArray2).inOrder();
+        assertThat(document.getPropertyDocumentArray("documentKey1")).asList()
+                .containsExactly(sDocumentProperties1, sDocumentProperties2).inOrder();
+    }
+
+    @Test
+    public void testDocument_toString() {
+        GenericDocument nestedDocValue = new GenericDocument.Builder<GenericDocument.Builder<?>>(
+                "namespace", "id2", "schemaType2")
+                .setCreationTimestampMillis(1L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyString("stringKey1", "val1", "val2")
+                .build();
+        GenericDocument document =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        "schemaType1")
+                        .setCreationTimestampMillis(1L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setPropertyString("stringKey1", "val1", "val2")
+                        .setPropertyBytes("bytesKey1", new byte[]{(byte) 1, (byte) 2})
+                        .setPropertyLong("longKey1", 1L, 2L)
+                        .setPropertyDouble("doubleKey1", 1.0, 2.0)
+                        .setPropertyBoolean("booleanKey1", true, false)
+                        .setPropertyDocument("documentKey1", nestedDocValue)
+                        .build();
+
+        String documentString = document.toString();
+
+        String expectedString = "{\n"
+                + "  namespace: \"namespace\",\n"
+                + "  id: \"id1\",\n"
+                + "  score: 1,\n"
+                + "  schemaType: \"schemaType1\",\n"
+                + "  creationTimestampMillis: 1,\n"
+                + "  timeToLiveMillis: 1,\n"
+                + "  properties: {\n"
+                + "    \"booleanKey1\": [true, false],\n"
+                + "    \"bytesKey1\": [[1, 2]],\n"
+                + "    \"documentKey1\": [\n"
+                + "      {\n"
+                + "        namespace: \"namespace\",\n"
+                + "        id: \"id2\",\n"
+                + "        score: 1,\n"
+                + "        schemaType: \"schemaType2\",\n"
+                + "        creationTimestampMillis: 1,\n"
+                + "        timeToLiveMillis: 1,\n"
+                + "        properties: {\n"
+                + "          \"stringKey1\": [\"val1\", \"val2\"]\n"
+                + "        }\n"
+                + "      }\n"
+                + "    ],\n"
+                + "    \"doubleKey1\": [1.0, 2.0],\n"
+                + "    \"longKey1\": [1, 2],\n"
+                + "    \"stringKey1\": [\"val1\", \"val2\"]\n"
+                + "  }\n"
+                + "}";
+
+        assertThat(documentString).isEqualTo(expectedString);
+    }
+
+    @Test
+    public void testDocumentGetValues_differentTypes() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setScore(1)
+                .setPropertyLong("longKey1", 1L)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        // Get a value for a key that doesn't exist
+        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(0.0);
+        assertThat(document.getPropertyDoubleArray("doubleKey1")).isNull();
+
+        // Get a value with a single element as an array and as a single value
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L);
+
+        // Get a value with multiple elements as an array and as a single value
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+        assertThat(document.getPropertyStringArray("stringKey1")).asList()
+                .containsExactly("test-value1", "test-value2", "test-value3").inOrder();
+
+        // Get a value of the wrong type
+        assertThat(document.getPropertyDouble("longKey1")).isEqualTo(0.0);
+        assertThat(document.getPropertyDoubleArray("longKey1")).isNull();
+    }
+
+    @Test
+    public void testDocument_setEmptyValues() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setPropertyBoolean("testKey")
+                .build();
+        assertThat(document.getPropertyBooleanArray("testKey")).isEmpty();
+    }
+
+    @Test
+    public void testDocumentInvalid() {
+        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        String nullString = null;
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setPropertyString("testKey", "string1", nullString));
+    }
+
+// @exportToFramework:startStrip()
+
+    // TODO(b/171882200): Expose this test in Android T
+    @Test
+    public void testDocument_toBuilder() {
+        GenericDocument document1 = new GenericDocument.Builder<>(
+                /*namespace=*/"", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "String1", "String2", "String3")
+                .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
+                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .build();
+        GenericDocument document2 = document1.toBuilder()
+                .setId("id2")
+                .setNamespace("namespace2")
+                .setPropertyBytes("byteKey1", sByteArray2)
+                .setPropertyLong("longKey2", 10L)
+                .clearProperty("booleanKey1")
+                .build();
+
+        // Make sure old doc hasn't changed
+        assertThat(document1.getId()).isEqualTo("id1");
+        assertThat(document1.getNamespace()).isEqualTo("");
+        assertThat(document1.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L).inOrder();
+        assertThat(document1.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true).inOrder();
+        assertThat(document1.getPropertyLongArray("longKey2")).isNull();
+
+        // Make sure the new doc contains the expected values
+        GenericDocument expectedDoc = new GenericDocument.Builder<>(
+                "namespace2", "id2", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyLong("longKey2", 10L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyString("stringKey1", "String1", "String2", "String3")
+                .setPropertyBytes("byteKey1", sByteArray2)
+                .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
+                .build();
+        assertThat(document2).isEqualTo(expectedDoc);
+    }
+
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testRetrieveTopLevelProperties() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+
+        // Top-level repeated properties should be retrievable
+        assertThat(doc.getPropertyStringArray("propString")).asList()
+                .containsExactly("Goodbye", "Hello").inOrder();
+        assertThat(doc.getPropertyLongArray("propInts")).asList()
+                .containsExactly(3L, 1L, 4L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDoubles")).usingTolerance(0.0001)
+                .containsExactly(3.14, 0.42).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propBools")).asList().containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{3, 4}});
+
+        // Top-level repeated properties should retrieve the first element
+        assertThat(doc.getPropertyString("propString")).isEqualTo("Goodbye");
+        assertThat(doc.getPropertyLong("propInts")).isEqualTo(3);
+        assertThat(doc.getPropertyDouble("propDoubles")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propBools")).isFalse();
+        assertThat(doc.getPropertyBytes("propBytes")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedProperties() {
+        GenericDocument innerDoc = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc)
+                .build();
+
+        // Document should be retrievable via both array and single getters
+        assertThat(doc.getPropertyDocument("propDocument")).isEqualTo(innerDoc);
+        assertThat(doc.getPropertyDocumentArray("propDocument")).asList()
+                .containsExactly(innerDoc);
+        assertThat((GenericDocument[]) doc.getProperty("propDocument")).asList()
+                .containsExactly(innerDoc);
+
+        // Nested repeated properties should be retrievable
+        assertThat(doc.getPropertyStringArray("propDocument.propString")).asList()
+                .containsExactly("Goodbye", "Hello").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propInts")).asList()
+                .containsExactly(3L, 1L, 4L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument.propDoubles")).usingTolerance(0.0001)
+                .containsExactly(3.14, 0.42).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument.propBools")).asList()
+                .containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}});
+        assertThat(doc.getProperty("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}});
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument.propString"))
+                .isEqualTo("Goodbye");
+        assertThat(doc.getPropertyLong("propDocument.propInts")).isEqualTo(3);
+        assertThat(doc.getPropertyDouble("propDocument.propDoubles")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propDocument.propBools")).isFalse();
+        assertThat(doc.getPropertyBytes("propDocument.propBytes")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesMultipleNestedDocuments() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Documents should be retrievable via both array and single getters
+        assertThat(doc.getPropertyDocument("propDocument")).isEqualTo(innerDoc0);
+        assertThat(doc.getPropertyDocumentArray("propDocument")).asList()
+                .containsExactly(innerDoc0, innerDoc1).inOrder();
+        assertThat((GenericDocument[]) doc.getProperty("propDocument")).asList()
+                .containsExactly(innerDoc0, innerDoc1).inOrder();
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument.propString")).asList()
+                .containsExactly("Goodbye", "Hello", "Aloha").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propInts")).asList()
+                .containsExactly(3L, 1L, 4L, 7L, 5L, 6L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument.propDoubles")).usingTolerance(0.0001)
+                .containsExactly(3.14, 0.42, 7.14, 0.356).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument.propBools")).asList()
+                .containsExactly(false, true).inOrder();
+        assertThat(doc.getPropertyBytesArray("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}, {8, 9}});
+        assertThat(doc.getProperty("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}, {8, 9}});
+
+        // Nested repeated properties should properly handle properties appearing in only one inner
+        // document, but not the other.
+        assertThat(
+                doc.getPropertyStringArray("propDocument.propStringTwo")).asList()
+                .containsExactly("Fee", "Fi").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propIntsTwo")).asList()
+                .containsExactly(8L, 6L).inOrder();
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument.propString"))
+                .isEqualTo("Goodbye");
+        assertThat(doc.getPropertyString("propDocument.propStringTwo"))
+                .isEqualTo("Fee");
+        assertThat(doc.getPropertyLong("propDocument.propInts")).isEqualTo(3);
+        assertThat(doc.getPropertyLong("propDocument.propIntsTwo")).isEqualTo(8L);
+        assertThat(doc.getPropertyDouble("propDocument.propDoubles")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propDocument.propBools")).isFalse();
+        assertThat(doc.getPropertyBytes("propDocument.propBytes")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveTopLevelPropertiesIndex() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+
+        // Top-level repeated properties should be retrievable
+        assertThat(doc.getPropertyStringArray("propString[1]")).asList()
+                .containsExactly("Hello");
+        assertThat(doc.getPropertyLongArray("propInts[2]")).asList()
+                .containsExactly(4L);
+        assertThat(doc.getPropertyDoubleArray("propDoubles[0]")).usingTolerance(0.0001)
+                .containsExactly(3.14);
+        assertThat(doc.getPropertyBooleanArray("propBools[0]")).asList().containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propBytes[0]")).isEqualTo(new byte[][]{{3, 4}});
+        assertThat(doc.getProperty("propBytes[0]")).isEqualTo(new byte[][]{{3, 4}});
+
+        // Top-level repeated properties should retrieve the first element
+        assertThat(doc.getPropertyString("propString[1]")).isEqualTo("Hello");
+        assertThat(doc.getPropertyLong("propInts[2]")).isEqualTo(4L);
+        assertThat(doc.getPropertyDouble("propDoubles[0]")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propBools[0]")).isFalse();
+        assertThat(doc.getPropertyBytes("propBytes[0]")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveTopLevelPropertiesIndexOutOfRange() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+
+        // Array getters should return null when given a bad index.
+        assertThat(doc.getPropertyStringArray("propString[5]")).isNull();
+
+        // Single getters should return default when given a bad index.
+        assertThat(doc.getPropertyDouble("propDoubles[7]")).isEqualTo(0.0);
+    }
+
+    @Test
+    public void testNestedProperties_unusualPaths() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setPropertyString("propString", "Hello", "Goodbye")
+                .setPropertyDocument("propDocs1", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyString("", "Cat", "Dog")
+                        .build())
+                .setPropertyDocument("propDocs2", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyDocument("", new GenericDocument.Builder<>("", "", "schema1")
+                                .setPropertyString("", "Red", "Blue")
+                                .setPropertyString("propString", "Bat", "Hawk")
+                                .build())
+                        .build())
+                .setPropertyDocument("", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyDocument("", new GenericDocument.Builder<>("", "", "schema1")
+                                .setPropertyString("", "Orange", "Green")
+                                .setPropertyString("propString", "Toad", "Bird")
+                                .build())
+                        .build())
+                .build();
+        assertThat(doc.getPropertyString("propString")).isEqualTo("Hello");
+        assertThat(doc.getPropertyString("propString[1]")).isEqualTo("Goodbye");
+        assertThat(doc.getPropertyString("propDocs1.")).isEqualTo("Cat");
+        assertThat(doc.getPropertyString("propDocs1.[1]")).isEqualTo("Dog");
+        assertThat(doc.getPropertyStringArray("propDocs1[0].")).asList()
+                .containsExactly("Cat", "Dog").inOrder();
+        assertThat(doc.getPropertyString("propDocs2..propString")).isEqualTo("Bat");
+        assertThat(doc.getPropertyString("propDocs2..propString[1]")).isEqualTo("Hawk");
+        assertThat(doc.getPropertyString("propDocs2..")).isEqualTo("Red");
+        assertThat(doc.getPropertyString("propDocs2..[1]")).isEqualTo("Blue");
+        assertThat(doc.getPropertyString("[0]..propString[1]")).isEqualTo("Bird");
+        assertThat(doc.getPropertyString("[0]..[1]")).isEqualTo("Green");
+    }
+
+    @Test
+    public void testNestedProperties_invalidPaths() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .setPropertyDocument("propDocs", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyString("", "Cat")
+                        .build())
+                .build();
+
+        // Some paths are invalid because they don't apply to the given document --- these should
+        // return null. It's not the querier's fault.
+        assertThat(doc.getPropertyStringArray("propString.propInts")).isNull();
+        assertThat(doc.getPropertyStringArray("propDocs.propFoo")).isNull();
+        assertThat(doc.getPropertyStringArray("propDocs.propNestedString.propFoo")).isNull();
+
+        // Some paths are invalid because they are malformed. These throw an exception --- the
+        // querier shouldn't provide such paths.
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propDocs.[0]propInts"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[0"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[0.]"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[banana]"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[-1]"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propDocs[0]cat"));
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesIntermediateIndex() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Documents should be retrievable via both array and single getters
+        assertThat(doc.getPropertyDocument("propDocument[1]")).isEqualTo(innerDoc1);
+        assertThat(doc.getPropertyDocumentArray("propDocument[1]")).asList()
+                .containsExactly(innerDoc1);
+        assertThat((GenericDocument[]) doc.getProperty("propDocument[1]")).asList()
+                .containsExactly(innerDoc1);
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument[1].propString")).asList()
+                .containsExactly("Aloha");
+        assertThat(doc.getPropertyLongArray("propDocument[0].propInts")).asList()
+                .containsExactly(3L, 1L, 4L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument[1].propDoubles")).usingTolerance(0.0001)
+                .containsExactly(7.14, 0.356).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument[0].propBools")).asList()
+                .containsExactly(false);
+        assertThat((boolean[]) doc.getProperty("propDocument[0].propBools")).asList()
+                .containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propDocument[1].propBytes")).isEqualTo(
+                new byte[][]{{8, 9}});
+
+        // Nested repeated properties should properly handle properties appearing in only one inner
+        // document, but not the other.
+        assertThat(doc.getPropertyStringArray("propDocument[0].propStringTwo")).asList()
+                .containsExactly("Fee", "Fi").inOrder();
+        assertThat(doc.getPropertyStringArray("propDocument[1].propStringTwo")).isNull();
+        assertThat(doc.getPropertyLongArray("propDocument[0].propIntsTwo")).isNull();
+        assertThat(doc.getPropertyLongArray("propDocument[1].propIntsTwo")).asList()
+                .containsExactly(8L, 6L).inOrder();
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument[1].propString"))
+                .isEqualTo("Aloha");
+        assertThat(doc.getPropertyString("propDocument[0].propStringTwo"))
+                .isEqualTo("Fee");
+        assertThat(doc.getPropertyLong("propDocument[1].propInts")).isEqualTo(7L);
+        assertThat(doc.getPropertyLong("propDocument[1].propIntsTwo")).isEqualTo(8L);
+        assertThat(doc.getPropertyDouble("propDocument[0].propDoubles"))
+                .isWithin(0.0001).of(3.14);
+        assertThat(doc.getPropertyBoolean("propDocument[1].propBools")).isTrue();
+        assertThat(doc.getPropertyBytes("propDocument[0].propBytes"))
+                .isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesLeafIndex() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument.propString[0]")).asList()
+                .containsExactly("Goodbye", "Aloha").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propInts[2]")).asList()
+                .containsExactly(4L, 6L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument.propDoubles[1]"))
+                .usingTolerance(0.0001).containsExactly(0.42, 0.356).inOrder();
+        assertThat((double[]) doc.getProperty("propDocument.propDoubles[1]"))
+                .usingTolerance(0.0001).containsExactly(0.42, 0.356).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument.propBools[0]")).asList()
+                .containsExactly(false, true).inOrder();
+        assertThat(doc.getPropertyBytesArray("propDocument.propBytes[0]"))
+                .isEqualTo(new byte[][]{{3, 4}, {8, 9}});
+
+        // Nested repeated properties should properly handle properties appearing in only one inner
+        // document, but not the other.
+        assertThat(doc.getPropertyStringArray("propDocument.propStringTwo[0]")).asList()
+                .containsExactly("Fee");
+        assertThat((String[]) doc.getProperty("propDocument.propStringTwo[0]")).asList()
+                .containsExactly("Fee");
+        assertThat(doc.getPropertyLongArray("propDocument.propIntsTwo[1]")).asList()
+                .containsExactly(6L);
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument.propString[1]"))
+                .isEqualTo("Hello");
+        assertThat(doc.getPropertyString("propDocument.propStringTwo[1]"))
+                .isEqualTo("Fi");
+        assertThat(doc.getPropertyLong("propDocument.propInts[1]"))
+                .isEqualTo(1L);
+        assertThat(doc.getPropertyLong("propDocument.propIntsTwo[1]")).isEqualTo(6L);
+        assertThat(doc.getPropertyDouble("propDocument.propDoubles[1]"))
+                .isWithin(0.0001).of(0.42);
+        assertThat(doc.getPropertyBoolean("propDocument.propBools[0]")).isFalse();
+        assertThat(doc.getPropertyBytes("propDocument.propBytes[0]"))
+                .isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesIntermediateAndLeafIndices() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument[1].propString[0]")).asList()
+                .containsExactly("Aloha");
+        assertThat(doc.getPropertyLongArray("propDocument[0].propInts[2]")).asList()
+                .containsExactly(4L);
+        assertThat((long[]) doc.getProperty("propDocument[0].propInts[2]")).asList()
+                .containsExactly(4L);
+        assertThat(doc.getPropertyDoubleArray("propDocument[1].propDoubles[1]"))
+                .usingTolerance(0.0001).containsExactly(0.356);
+        assertThat(doc.getPropertyBooleanArray("propDocument[0].propBools[0]")).asList()
+                .containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propDocument[1].propBytes[0]"))
+                .isEqualTo(new byte[][]{{8, 9}});
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument[0].propString[1]"))
+                .isEqualTo("Hello");
+        assertThat(doc.getPropertyString("propDocument[0].propStringTwo[1]"))
+                .isEqualTo("Fi");
+        assertThat(doc.getPropertyLong("propDocument[1].propInts[1]"))
+                .isEqualTo(5L);
+        assertThat(doc.getPropertyLong("propDocument[1].propIntsTwo[1]"))
+                .isEqualTo(6L);
+        assertThat(doc.getPropertyDouble("propDocument[0].propDoubles[1]"))
+                .isWithin(0.0001).of(0.42);
+        assertThat(doc.getPropertyBoolean("propDocument[1].propBools[0]")).isTrue();
+        assertThat(doc.getPropertyBytes("propDocument[0].propBytes[0]"))
+                .isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testDocumentGetPropertyNamesSingleLevel() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L)
+                .setPropertyDouble("doubleKey1", 1.0)
+                .setPropertyBoolean("booleanKey1", true)
+                .setPropertyString("stringKey1", "test-value1")
+                .setPropertyBytes("byteKey1", sByteArray1)
+                .build();
+        assertThat(document.getPropertyNames()).containsExactly("longKey1", "doubleKey1",
+                "booleanKey1", "stringKey1", "byteKey1");
+    }
+
+    @Test
+    public void testDocumentGetPropertyNamesMultiLevel() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .build();
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyString("stringKey1", "test-value1")
+                .setPropertyDocument("docKey1", innerDoc0, innerDoc1)
+                .build();
+        assertThat(document.getPropertyNames()).containsExactly("stringKey1", "docKey1");
+
+        GenericDocument[] documents = document.getPropertyDocumentArray("docKey1");
+        assertThat(documents).asList().containsExactly(innerDoc0, innerDoc1).inOrder();
+        assertThat(documents[0].getPropertyNames()).containsExactly("propString", "propStringTwo",
+                "propInts");
+        assertThat(documents[1].getPropertyNames()).containsExactly("propString", "propInts",
+                "propIntsTwo");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetByDocumentIdRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetByDocumentIdRequestCtsTest.java
new file mode 100644
index 0000000..f9c1e6a
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetByDocumentIdRequestCtsTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GetByDocumentIdRequest;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class GetByDocumentIdRequestCtsTest {
+    @Test
+    public void testBuildRequest() {
+        List<String> expectedPropertyPaths1 = Arrays.asList("path1", "path2");
+        List<String> expectedPropertyPaths2 = Arrays.asList("path3", "path4");
+        GetByDocumentIdRequest getByDocumentIdRequest =
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds("uri1", "uri2")
+                        .addIds(Arrays.asList("uri3", "uri4"))
+                        .addProjection("schemaType1", expectedPropertyPaths1)
+                        .addProjection("schemaType2", expectedPropertyPaths2)
+                        .build();
+
+        assertThat(getByDocumentIdRequest.getIds()).containsExactly(
+                "uri1", "uri2", "uri3", "uri4");
+        assertThat(getByDocumentIdRequest.getNamespace()).isEqualTo("namespace");
+        assertThat(getByDocumentIdRequest.getProjections())
+                .containsExactly("schemaType1", expectedPropertyPaths1, "schemaType2",
+                        expectedPropertyPaths2);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
new file mode 100644
index 0000000..1220731
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GetSchemaResponse;
+
+import org.junit.Test;
+
+public class GetSchemaResponseCtsTest {
+    @Test
+    public void testRebuild() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        GetSchemaResponse.Builder builder =
+                new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1);
+
+        GetSchemaResponse original = builder.build();
+        GetSchemaResponse rebuild = builder.setVersion(37).addSchema(schema2).build();
+
+        // rebuild won't effect the original object
+        assertThat(original.getVersion()).isEqualTo(42);
+        assertThat(original.getSchemas()).containsExactly(schema1);
+
+        assertThat(rebuild.getVersion()).isEqualTo(37);
+        assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
new file mode 100644
index 0000000..ca92a66
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
@@ -0,0 +1,747 @@
+/*
+ * 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 androidx.appsearch.cts.app;
+
+import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.ReportSystemUsageRequest;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.util.AppSearchEmail;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public abstract class GlobalSearchSessionCtsTestBase {
+    private AppSearchSession mDb1;
+    private static final String DB_NAME_1 = "";
+    private AppSearchSession mDb2;
+    private static final String DB_NAME_2 = "testDb2";
+
+    private GlobalSearchSession mGlobalAppSearchManager;
+
+    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull String dbName);
+
+    protected abstract ListenableFuture<GlobalSearchSession> createGlobalSearchSession();
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        mDb2 = createSearchSession(DB_NAME_2).get();
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        cleanup();
+
+        mGlobalAppSearchManager = createGlobalSearchSession().get();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        cleanup();
+    }
+
+    private void cleanup() throws Exception {
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    private List<GenericDocument> snapshotResults(String queryExpression, SearchSpec spec)
+            throws Exception {
+        SearchResults searchResults = mGlobalAppSearchManager.search(queryExpression, spec);
+        return convertSearchResultsToDocuments(searchResults);
+    }
+
+    /**
+     * Asserts that the union of {@code addedDocuments} and {@code beforeDocuments} is exactly
+     * equivalent to {@code afterDocuments}. Order doesn't matter.
+     *
+     * @param beforeDocuments Documents that existed first.
+     * @param afterDocuments  The total collection of documents that should exist now.
+     * @param addedDocuments  The collection of documents that were expected to be added.
+     */
+    private void assertAddedBetweenSnapshots(List<? extends GenericDocument> beforeDocuments,
+            List<? extends GenericDocument> afterDocuments,
+            List<? extends GenericDocument> addedDocuments) {
+        List<GenericDocument> expectedDocuments = new ArrayList<>(beforeDocuments);
+        expectedDocuments.addAll(addedDocuments);
+        assertThat(afterDocuments).containsExactlyElementsIn(expectedDocuments);
+    }
+
+    @Test
+    public void testGlobalQuery_oneInstance() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+        List<GenericDocument> beforeBodyEmailDocuments = snapshotResults("body email",
+                exactSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
+                Collections.singletonList(inEmail));
+
+        // Multi-term query
+        List<GenericDocument> afterBodyEmailDocuments = snapshotResults("body email",
+                exactSearchSpec);
+        assertAddedBetweenSnapshots(beforeBodyEmailDocuments, afterBodyEmailDocuments,
+                Collections.singletonList(inEmail));
+    }
+
+    @Test
+    public void testGlobalQuery_twoInstances() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document to instance 1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+
+        // Index a document to instance 2.
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Query across all instances
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
+                ImmutableList.of(inEmail1, inEmail2));
+    }
+
+    @Test
+    public void testGlobalQuery_getNextPage() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        List<AppSearchEmail> emailList = new ArrayList<>();
+        PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
+
+        // Index 31 documents
+        for (int i = 0; i < 31; i++) {
+            AppSearchEmail inEmail =
+                    new AppSearchEmail.Builder("namespace", "id" + i)
+                            .setFrom("from@example.com")
+                            .setTo("to1@example.com", "to2@example.com")
+                            .setSubject("testPut example")
+                            .setBody("This is the body of the testPut email")
+                            .build();
+            emailList.add(inEmail);
+            putDocumentsRequestBuilder.addGenericDocuments(inEmail);
+        }
+        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+
+        // Set number of results per page is 7.
+        int pageSize = 7;
+        SearchResults searchResults = mGlobalAppSearchManager.search("body",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultCountPerPage(pageSize)
+                        .build());
+        List<GenericDocument> documents = new ArrayList<>();
+
+        int pageNumber = 0;
+        List<SearchResult> results;
+
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            ++pageNumber;
+            for (SearchResult result : results) {
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+
+        // check all document presents
+        assertAddedBetweenSnapshots(beforeBodyDocuments, documents, emailList);
+
+        int totalDocuments = beforeBodyDocuments.size() + documents.size();
+
+        // +1 for final empty page
+        int expectedPages = (int) Math.ceil(totalDocuments * 1.0 / pageSize) + 1;
+        assertThat(pageNumber).isEqualTo(expectedPages);
+    }
+
+    @Test
+    public void testGlobalQuery_acrossTypes() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        SearchSpec exactEmailSearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                        .build();
+        List<GenericDocument> beforeBodyEmailDocuments = snapshotResults("body",
+                exactEmailSearchSpec);
+
+        // Schema registration
+        AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("foo")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build()
+                ).build();
+
+        // db1 has both "Generic" and "builtin:Email"
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(genericSchema).addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // db2 only has "builtin:Email"
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a generic document into db1
+        GenericDocument genericDocument = new GenericDocument.Builder<>("namespace", "id2",
+                "Generic")
+                .setPropertyString("foo", "body").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(genericDocument).build()));
+
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Put the email in both databases
+        checkIsBatchResultSuccess((mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build())));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Query for all documents across types
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
+                ImmutableList.of(genericDocument, email, email));
+
+        // Query only for email documents
+        List<GenericDocument> afterBodyEmailDocuments = snapshotResults("body",
+                exactEmailSearchSpec);
+        assertAddedBetweenSnapshots(beforeBodyEmailDocuments, afterBodyEmailDocuments,
+                ImmutableList.of(email, email));
+    }
+
+    @Test
+    public void testGlobalQuery_namespaceFilter() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec exactSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+        List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
+
+        SearchSpec exactNamespace1SearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterNamespaces("namespace1")
+                        .build();
+        List<GenericDocument> beforeBodyNamespace1Documents = snapshotResults("body",
+                exactNamespace1SearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index two documents
+        AppSearchEmail document1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(document1).build()));
+
+        AppSearchEmail document2 =
+                new AppSearchEmail.Builder("namespace2", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
+
+        // Query for all namespaces
+        List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
+        assertAddedBetweenSnapshots(beforeBodyDocuments, afterBodyDocuments,
+                ImmutableList.of(document1, document2));
+
+        // Query only for "namespace1"
+        List<GenericDocument> afterBodyNamespace1Documents = snapshotResults("body",
+                exactNamespace1SearchSpec);
+        assertAddedBetweenSnapshots(beforeBodyNamespace1Documents, afterBodyNamespace1Documents,
+                ImmutableList.of(document1));
+    }
+
+    @Test
+    public void testGlobalQuery_packageFilter() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec otherPackageSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterPackageNames("some.other.package")
+                .build();
+        List<GenericDocument> beforeOtherPackageDocuments = snapshotResults("body",
+                otherPackageSearchSpec);
+
+        SearchSpec testPackageSearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterPackageNames(
+                                ApplicationProvider.getApplicationContext().getPackageName())
+                        .build();
+        List<GenericDocument> beforeTestPackageDocuments = snapshotResults("body",
+                testPackageSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index two documents
+        AppSearchEmail document1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(document1).build()));
+
+        AppSearchEmail document2 =
+                new AppSearchEmail.Builder("namespace2", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
+
+        // Query in some other package
+        List<GenericDocument> afterOtherPackageDocuments = snapshotResults("body",
+                otherPackageSearchSpec);
+        assertAddedBetweenSnapshots(beforeOtherPackageDocuments, afterOtherPackageDocuments,
+                Collections.emptyList());
+
+        // Query within our package
+        List<GenericDocument> afterTestPackageDocuments = snapshotResults("body",
+                testPackageSearchSpec);
+        assertAddedBetweenSnapshots(beforeTestPackageDocuments, afterTestPackageDocuments,
+                ImmutableList.of(document1, document2));
+    }
+
+    // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
+    @Test
+    public void testGlobalQuery_projectionTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email2).build()));
+
+        // Query with type property paths {"Email", ["subject", "to"]}
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addProjection(
+                                AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                        .build());
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email2).build()));
+
+        // Query with type property paths {"Email", []}
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addProjection(AppSearchEmail.SCHEMA_TYPE,
+                                Collections.emptyList())
+                        .build());
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email2).build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addProjection("NonExistentType", Collections.emptyList())
+                        .addProjection(
+                                AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                        .build());
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testQuery_ResultGroupingLimits() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index one document in 'namespace1' and one document in 'namespace2' into db1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace2", "id2")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Index one document in 'namespace1' and one document in 'namespace2' into db2.
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace1", "id3")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // Query with per package result grouping. Only the last document 'email4' should be
+        // returned.
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultGrouping(
+                                SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                        .build());
+        assertThat(documents).containsExactly(inEmail4);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('email4' and 'email3').
+        documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultGrouping(
+                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                        .build());
+        assertThat(documents).containsExactly(inEmail4, inEmail3);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('email4' and 'email3').
+        documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultGrouping(
+                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                        .build());
+        assertThat(documents).containsExactly(inEmail4, inEmail3);
+    }
+
+    @Test
+    public void testReportSystemUsage_ForbiddenFromNonSystem() throws Exception {
+        // Index a document
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Query
+        List<SearchResult> page;
+        try (SearchResults results = mGlobalAppSearchManager.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                .build())) {
+            page = results.getNextPage().get();
+        }
+        assertThat(page).isNotEmpty();
+        SearchResult firstResult = page.get(0);
+
+        ExecutionException exception = assertThrows(
+                ExecutionException.class, () -> mGlobalAppSearchManager.reportSystemUsage(
+                        new ReportSystemUsageRequest.Builder(
+                                firstResult.getPackageName(),
+                                firstResult.getDatabaseName(),
+                                firstResult.getGenericDocument().getNamespace(),
+                                firstResult.getGenericDocument().getId())
+                                .build()).get());
+        assertThat(exception).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException ase = (AppSearchException) exception.getCause();
+        assertThat(ase.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+        assertThat(ase).hasMessageThat().contains(
+                "androidx.appsearch.test does not have access to report system usage");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
similarity index 86%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
index 4586ca1..b1d82ba6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 // @exportToFramework:skipFile()
-package androidx.appsearch.app.cts;
+package androidx.appsearch.cts.app;
 
 import android.content.Context;
 
@@ -26,14 +26,12 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-// TODO(b/175801531): Support this test for the platform backend once the global search API is
-//  public.
 public class GlobalSearchSessionLocalCtsTest extends GlobalSearchSessionCtsTestBase {
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
similarity index 68%
copy from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
copy to appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
index 4586ca1..a5fd1fe 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
@@ -14,32 +14,33 @@
  * limitations under the License.
  */
 // @exportToFramework:skipFile()
-package androidx.appsearch.app.cts;
+package androidx.appsearch.cts.app;
 
 import android.content.Context;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
-import androidx.appsearch.localstorage.LocalStorage;
+import androidx.appsearch.platformstorage.PlatformStorage;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-// TODO(b/175801531): Support this test for the platform backend once the global search API is
-//  public.
-public class GlobalSearchSessionLocalCtsTest extends GlobalSearchSessionCtsTestBase {
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class GlobalSearchSessionPlatformCtsTest extends GlobalSearchSessionCtsTestBase {
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
     protected ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createGlobalSearchSession(
-                new LocalStorage.GlobalSearchContext.Builder(context).build());
+        return PlatformStorage.createGlobalSearchSession(
+                new PlatformStorage.GlobalSearchContext.Builder(context).build());
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PackageIdentifierCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PackageIdentifierCtsTest.java
new file mode 100644
index 0000000..bae9e22
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PackageIdentifierCtsTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.PackageIdentifier;
+
+import org.junit.Test;
+
+public class PackageIdentifierCtsTest {
+    @Test
+    public void testGetters() {
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.packageName",
+                /*sha256Certificate=*/ new byte[]{100});
+        assertThat(packageIdentifier.getPackageName()).isEqualTo("com.packageName");
+        assertThat(packageIdentifier.getSha256Certificate()).isEqualTo(new byte[]{100});
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
new file mode 100644
index 0000000..7753e15
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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 androidx.appsearch.cts.app;
+
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.util.AppSearchEmail;
+import androidx.appsearch.localstorage.LocalStorage;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class PutDocumentsRequestCtsTest {
+
+    @Test
+    public void addGenericDocument_byCollection() {
+        Set<AppSearchEmail> emails =
+                ImmutableSet.of(new AppSearchEmail.Builder("namespace", "test1").build(),
+                        new AppSearchEmail.Builder("namespace", "test2").build());
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addGenericDocuments(emails)
+                .build();
+
+        assertThat(request.getGenericDocuments().get(0).getId()).isEqualTo("test1");
+        assertThat(request.getGenericDocuments().get(1).getId()).isEqualTo("test2");
+    }
+
+// @exportToFramework:startStrip()
+    @Document
+    static class Card {
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.StringProperty(indexingType = INDEXING_TYPE_PREFIXES)
+        String mString;
+
+        Card(String namespace, String id, String string) {
+            mId = id;
+            mNamespace = namespace;
+            mString = string;
+        }
+    }
+
+    @Test
+    public void addDocumentClasses_byCollection() throws Exception {
+        // A schema with Card must be set in order to be able to add a Card instance to
+        // PutDocumentsRequest.
+        Context context = ApplicationProvider.getApplicationContext();
+        AppSearchSession session = LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(context, /*databaseName=*/ "")
+                        .build()
+        ).get();
+        session.setSchema(new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build())
+            .get();
+
+        Set<Card> cards = ImmutableSet.of(new Card("cardNamespace", "cardId", "cardProperty"));
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(cards)
+                .build();
+
+        assertThat(request.getGenericDocuments().get(0).getId()).isEqualTo("cardId");
+    }
+// @exportToFramework:endStrip()
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/RemoveByDocumentIdRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/RemoveByDocumentIdRequestCtsTest.java
new file mode 100644
index 0000000..9fe1ebb
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/RemoveByDocumentIdRequestCtsTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class RemoveByDocumentIdRequestCtsTest {
+    @Test
+    public void testBuildRequest() {
+        RemoveByDocumentIdRequest request = new RemoveByDocumentIdRequest.Builder("namespace")
+                .addIds("uri1", "uri2")
+                .addIds(Arrays.asList("uri3"))
+                .build();
+
+        assertThat(request.getNamespace()).isEqualTo("namespace");
+        assertThat(request.getIds()).containsExactly("uri1", "uri2", "uri3").inOrder();
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/ReportSystemUsageRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/ReportSystemUsageRequestCtsTest.java
new file mode 100644
index 0000000..69331f8
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/ReportSystemUsageRequestCtsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.ReportSystemUsageRequest;
+
+import org.junit.Test;
+
+public class ReportSystemUsageRequestCtsTest {
+    @Test
+    public void testGettersAndSetters() {
+        ReportSystemUsageRequest request = new ReportSystemUsageRequest.Builder(
+                "package1", "database1", "namespace1", "id1")
+                .setUsageTimestampMillis(32)
+                .build();
+        assertThat(request.getPackageName()).isEqualTo("package1");
+        assertThat(request.getDatabaseName()).isEqualTo("database1");
+        assertThat(request.getNamespace()).isEqualTo("namespace1");
+        assertThat(request.getDocumentId()).isEqualTo("id1");
+        assertThat(request.getUsageTimestampMillis()).isEqualTo(32);
+    }
+
+    @Test
+    public void testUsageTimestampDefault() {
+        long startTs = System.currentTimeMillis();
+        ReportSystemUsageRequest request =
+                new ReportSystemUsageRequest.Builder("package1", "database1", "namespace1", "id1")
+                        .build();
+        assertThat(request.getUsageTimestampMillis()).isAtLeast(startTs);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/ReportUsageRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/ReportUsageRequestCtsTest.java
new file mode 100644
index 0000000..a873c33
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/ReportUsageRequestCtsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.ReportUsageRequest;
+
+import org.junit.Test;
+
+public class ReportUsageRequestCtsTest {
+    @Test
+    public void testGettersAndSetters() {
+        ReportUsageRequest request = new ReportUsageRequest.Builder("namespace1", "id1")
+                .setUsageTimestampMillis(32)
+                .build();
+        assertThat(request.getNamespace()).isEqualTo("namespace1");
+        assertThat(request.getDocumentId()).isEqualTo("id1");
+        assertThat(request.getUsageTimestampMillis()).isEqualTo(32);
+    }
+
+    @Test
+    public void testUsageTimestampDefault() {
+        long startTs = System.currentTimeMillis();
+        ReportUsageRequest request = new ReportUsageRequest.Builder("namespace1", "id1").build();
+        assertThat(request.getUsageTimestampMillis()).isAtLeast(startTs);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
new file mode 100644
index 0000000..ea3efe8
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.util.AppSearchEmail;
+
+import org.junit.Test;
+
+public class SearchResultCtsTest {
+
+    @Test
+    public void testBuildSearchResult() {
+        SearchResult.MatchRange exactMatchRange = new SearchResult.MatchRange(3, 8);
+        SearchResult.MatchRange snippetMatchRange = new SearchResult.MatchRange(1, 10);
+        SearchResult.MatchInfo matchInfo =
+                new SearchResult.MatchInfo.Builder("body")
+                        .setExactMatchRange(exactMatchRange)
+                        .setSnippetRange(snippetMatchRange).build();
+
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+                .setBody("Hello World.")
+                .build();
+        SearchResult searchResult = new SearchResult.Builder("packageName", "databaseName")
+                .setGenericDocument(email)
+                .addMatchInfo(matchInfo)
+                .setRankingSignal(2.9)
+                .build();
+
+        assertThat(searchResult.getPackageName()).isEqualTo("packageName");
+        assertThat(searchResult.getDatabaseName()).isEqualTo("databaseName");
+        assertThat(searchResult.getRankingSignal()).isEqualTo(2.9);
+        assertThat(searchResult.getGenericDocument()).isEqualTo(email);
+        assertThat(searchResult.getMatchInfos()).hasSize(1);
+        SearchResult.MatchInfo actualMatchInfo = searchResult.getMatchInfos().get(0);
+        assertThat(actualMatchInfo.getPropertyPath()).isEqualTo("body");
+        assertThat(actualMatchInfo.getExactMatchRange()).isEqualTo(exactMatchRange);
+        assertThat(actualMatchInfo.getSnippetRange()).isEqualTo(snippetMatchRange);
+        assertThat(actualMatchInfo.getExactMatch()).isEqualTo("lo Wo");
+        assertThat(actualMatchInfo.getSnippet()).isEqualTo("ello Worl");
+        assertThat(actualMatchInfo.getFullText()).isEqualTo("Hello World.");
+    }
+
+    @Test
+    public void testMatchRange() {
+        SearchResult.MatchRange matchRange = new SearchResult.MatchRange(13, 47);
+        assertThat(matchRange.getStart()).isEqualTo(13);
+        assertThat(matchRange.getEnd()).isEqualTo(47);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
new file mode 100644
index 0000000..c62ba4f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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 androidx.appsearch.cts.app;
+
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.SearchSpec;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class SearchSpecCtsTest {
+    @Test
+    public void testBuildSearchSpecWithoutTermMatch() {
+        SearchSpec searchSpec = new SearchSpec.Builder().addFilterSchemas("testSchemaType").build();
+        assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
+    }
+
+    @Test
+    public void testBuildSearchSpec() {
+        List<String> expectedPropertyPaths1 = ImmutableList.of("path1", "path2");
+        List<String> expectedPropertyPaths2 = ImmutableList.of("path3", "path4");
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterNamespaces("namespace1", "namespace2")
+                .addFilterNamespaces(ImmutableList.of("namespace3"))
+                .addFilterSchemas("schemaTypes1", "schemaTypes2")
+                .addFilterSchemas(ImmutableList.of("schemaTypes3"))
+                .addFilterPackageNames("package1", "package2")
+                .addFilterPackageNames(ImmutableList.of("package3"))
+                .setSnippetCount(5)
+                .setSnippetCountPerProperty(10)
+                .setMaxSnippetSize(15)
+                .setResultCountPerPage(42)
+                .setOrder(SearchSpec.ORDER_ASCENDING)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 37)
+                .addProjection("schemaType1", expectedPropertyPaths1)
+                .addProjection("schemaType2", expectedPropertyPaths2)
+                .build();
+
+        assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
+        assertThat(searchSpec.getFilterNamespaces())
+                .containsExactly("namespace1", "namespace2", "namespace3").inOrder();
+        assertThat(searchSpec.getFilterSchemas())
+                .containsExactly("schemaTypes1", "schemaTypes2", "schemaTypes3").inOrder();
+        assertThat(searchSpec.getFilterPackageNames())
+                .containsExactly("package1", "package2", "package3").inOrder();
+        assertThat(searchSpec.getSnippetCount()).isEqualTo(5);
+        assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10);
+        assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15);
+        assertThat(searchSpec.getResultCountPerPage()).isEqualTo(42);
+        assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+        assertThat(searchSpec.getRankingStrategy())
+                .isEqualTo(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE);
+        assertThat(searchSpec.getResultGroupingTypeFlags())
+                .isEqualTo(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE);
+        assertThat(searchSpec.getProjections())
+                .containsExactly("schemaType1", expectedPropertyPaths1, "schemaType2",
+                        expectedPropertyPaths2);
+        assertThat(searchSpec.getResultGroupingLimit()).isEqualTo(37);
+    }
+
+    @Test
+    public void testGetProjectionTypePropertyMasks() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addProjection("TypeA", ImmutableList.of("field1", "field2.subfield2"))
+                .addProjection("TypeB", ImmutableList.of("field7"))
+                .addProjection("TypeC", ImmutableList.of())
+                .build();
+
+        Map<String, List<String>> typePropertyPathMap = searchSpec.getProjections();
+        assertThat(typePropertyPathMap.keySet())
+                .containsExactly("TypeA", "TypeB", "TypeC");
+        assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
+        assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
+        assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+    }
+
+// @exportToFramework:startStrip()
+    @Document
+    static class King extends Card {
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.StringProperty
+                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
+        String mString;
+    }
+
+    static class Card {}
+
+    @Test
+    public void testFilterDocumentClasses_byCollection() throws Exception {
+        Set<Class<King>> cardClassSet = ImmutableSet.of(King.class);
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterDocumentClasses(cardClassSet)
+                .build();
+
+        assertThat(searchSpec.getFilterSchemas()).containsExactly("King");
+    }
+// @exportToFramework:endStrip()
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
new file mode 100644
index 0000000..d00bf30
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.util.AppSearchEmail;
+import androidx.collection.ArrayMap;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class SetSchemaRequestCtsTest {
+    @Test
+    public void testBuildSetSchemaRequest() {
+        AppSearchSchema.StringPropertyConfig prop1 =
+                new AppSearchSchema.StringPropertyConfig.Builder("prop1")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build();
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("type1").addProperty(prop1).build();
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("type2").addProperty(prop1).build();
+        AppSearchSchema schema3 =
+                new AppSearchSchema.Builder("type3").addProperty(prop1).build();
+        AppSearchSchema schema4 =
+                new AppSearchSchema.Builder("type4").addProperty(prop1).build();
+
+        PackageIdentifier packageIdentifier =
+                new PackageIdentifier("com.package.foo", new byte[]{100});
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema1, schema2)
+                .addSchemas(Arrays.asList(schema3, schema4))
+                .setSchemaTypeDisplayedBySystem("type2", /*displayed=*/ false)
+                .setSchemaTypeVisibilityForPackage("type1", /*visible=*/ true,
+                        packageIdentifier)
+                .setForceOverride(true)
+                .setVersion(142857)
+                .build();
+
+        assertThat(request.getSchemas()).containsExactly(schema1, schema2, schema3, schema4);
+        assertThat(request.getSchemasNotDisplayedBySystem()).containsExactly("type2");
+
+        assertThat(request.getSchemasVisibleToPackages()).containsExactly(
+                "type1", Collections.singleton(packageIdentifier));
+        assertThat(request.getVersion()).isEqualTo(142857);
+        assertThat(request.isForceOverride()).isTrue();
+    }
+
+    @Test
+    public void testSetSchemaRequestTypeChanges() {
+        AppSearchSchema.StringPropertyConfig requiredProp =
+                new AppSearchSchema.StringPropertyConfig.Builder("prop1")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build();
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("type1").addProperty(requiredProp).build();
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("type2").addProperty(requiredProp).build();
+        AppSearchSchema schema3 =
+                new AppSearchSchema.Builder("type3").addProperty(requiredProp).build();
+
+        Migrator expectedMigrator1 = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+        Migrator expectedMigrator2 = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+        Migrator expectedMigrator3 = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+        Map<String, Migrator> migratorMap = new ArrayMap<>();
+        migratorMap.put("type1", expectedMigrator1);
+        migratorMap.put("type2", expectedMigrator2);
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema1, schema2, schema3)
+                .setForceOverride(/*forceOverride=*/ true)
+                .setMigrators(migratorMap)
+                .setMigrator("type3", expectedMigrator3)
+                .build();
+
+        assertThat(request.isForceOverride()).isTrue();
+        Map<String, Migrator> expectedMigratorMap = new ArrayMap<>();
+        expectedMigratorMap.put("type1", expectedMigrator1);
+        expectedMigratorMap.put("type2", expectedMigrator2);
+        expectedMigratorMap.put("type3", expectedMigrator3);
+        assertThat(request.getMigrators()).containsExactlyEntriesIn(expectedMigratorMap);
+    }
+
+    @Test
+    public void testInvalidSchemaReferences_fromDisplayedBySystem() {
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder().setSchemaTypeDisplayedBySystem(
+                        "InvalidSchema", false).build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
+    public void testInvalidSchemaReferences_fromPackageVisibility() {
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForPackage(
+                        "InvalidSchema", /*visible=*/ true, new PackageIdentifier(
+                                "com.foo.package", /*sha256Certificate=*/ new byte[]{})).build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
+    public void testSetSchemaTypeDisplayedBySystem_displayed() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        // By default, the schema is displayed.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
+
+        request = new SetSchemaRequest.Builder()
+                .addSchemas(schema).setSchemaTypeDisplayedBySystem("Schema", true).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
+    }
+
+    @Test
+    public void testSetSchemaTypeDisplayedBySystem_notDisplayed() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema).setSchemaTypeDisplayedBySystem("Schema", false).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).containsExactly("Schema");
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_visible() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).build();
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+
+        request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setSchemaTypeVisibilityForPackage(
+                        "Schema", /*visible=*/ true, packageIdentifier).build();
+        assertThat(request.getSchemasVisibleToPackages()).containsExactly(
+                "Schema", Collections.singleton(packageIdentifier));
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_notVisible() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setSchemaTypeVisibilityForPackage(
+                        "Schema", /*visible=*/ false, new PackageIdentifier("com.package.foo",
+                                /*sha256Certificate=*/ new byte[]{})).build();
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_deduped() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(schema)
+                        // Set it visible for "Schema"
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                true, packageIdentifier)
+                        // Set it visible for "Schema" again, which should be a no-op
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                true, packageIdentifier)
+                        .build();
+        assertThat(request.getSchemasVisibleToPackages()).containsExactly(
+                "Schema", Collections.singleton(packageIdentifier));
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForPackage_removed() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(schema)
+                        // First set it as visible
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                true, new PackageIdentifier("com.package.foo",
+                                        /*sha256Certificate=*/ new byte[]{100}))
+                        // Then make it not visible
+                        .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+                                false, new PackageIdentifier("com.package.foo",
+                                        /*sha256Certificate=*/ new byte[]{100}))
+                        .build();
+
+        // Nothing should be visible.
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+    }
+
+
+// @exportToFramework:startStrip()
+    @Document
+    static class Card {
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.StringProperty
+                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
+        String mString;
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof Card)) {
+                return false;
+            }
+            Card otherCard = (Card) other;
+            assertThat(otherCard.mNamespace).isEqualTo(this.mNamespace);
+            assertThat(otherCard.mId).isEqualTo(this.mId);
+            return true;
+        }
+    }
+
+    static class Spade {}
+
+    @Document
+    static class King extends Spade {
+        @Document.Id
+        String mId;
+
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.StringProperty
+                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
+        String mString;
+    }
+
+    @Document
+    static class Queen extends Spade {
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.StringProperty
+                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
+        String mString;
+    }
+
+    private static Collection<String> getSchemaTypesFromSetSchemaRequest(SetSchemaRequest request) {
+        HashSet<String> schemaTypes = new HashSet<>();
+        for (AppSearchSchema schema : request.getSchemas()) {
+            schemaTypes.add(schema.getSchemaType());
+        }
+        return schemaTypes;
+    }
+
+    @Test
+    public void testSetDocumentClassDisplayedBySystem_displayed() throws Exception {
+        // By default, the schema is displayed.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
+
+        request =
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassDisplayedBySystem(
+                        Card.class, true).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
+    }
+
+    @Test
+    public void testSetDocumentClassDisplayedBySystem_notDisplayed() throws Exception {
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassDisplayedBySystem(
+                        Card.class, false).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).containsExactly("Card");
+    }
+
+    @Test
+    public void testSetDocumentClassVisibilityForPackage_visible() throws Exception {
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        Map<String, Set<PackageIdentifier>> expectedVisibleToPackagesMap = new ArrayMap<>();
+        expectedVisibleToPackagesMap.put("Card", Collections.singleton(packageIdentifier));
+
+        request =
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassVisibilityForPackage(
+                        Card.class, /*visible=*/ true, packageIdentifier).build();
+        assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
+                expectedVisibleToPackagesMap);
+    }
+
+    @Test
+    public void testSetDocumentClassVisibilityForPackage_notVisible() throws Exception {
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassVisibilityForPackage(
+                        Card.class, /*visible=*/ false,
+                        new PackageIdentifier("com.package.foo", /*sha256Certificate=*/
+                                new byte[]{})).build();
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+    }
+
+    @Test
+    public void testSetDocumentClassVisibilityForPackage_deduped() throws Exception {
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+
+        PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        Map<String, Set<PackageIdentifier>> expectedVisibleToPackagesMap = new ArrayMap<>();
+        expectedVisibleToPackagesMap.put("Card", Collections.singleton(packageIdentifier));
+
+        request =
+                new SetSchemaRequest.Builder()
+                        .addDocumentClasses(Card.class)
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
+                                true, packageIdentifier)
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
+                                true, packageIdentifier)
+                        .build();
+        assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
+                expectedVisibleToPackagesMap);
+    }
+
+    @Test
+    public void testSetDocumentClassVisibilityForPackage_removed() throws Exception {
+        // By default, the schema is not visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+
+        request =
+                new SetSchemaRequest.Builder()
+                        .addDocumentClasses(Card.class)
+                        // First set it as visible
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
+                                true, new PackageIdentifier("com.package.foo",
+                                        /*sha256Certificate=*/ new byte[]{100}))
+                        // Then make it not visible
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
+                                false, new PackageIdentifier("com.package.foo",
+                                        /*sha256Certificate=*/ new byte[]{100}))
+                        .build();
+
+        // Nothing should be visible.
+        assertThat(request.getSchemasVisibleToPackages()).isEmpty();
+    }
+
+    @Test
+    public void testAddDocumentClasses_byCollection() throws Exception {
+        Set<Class<? extends Spade>> cardClasses = ImmutableSet.of(Queen.class, King.class);
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(cardClasses)
+                        .build();
+        assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
+                "King");
+    }
+
+    @Test
+    public void testAddDocumentClasses_byCollectionWithSeparateCalls() throws
+            Exception {
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(ImmutableSet.of(Queen.class))
+                        .addDocumentClasses(ImmutableSet.of(King.class)).build();
+        assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
+                "King");
+    }
+
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testSetVersion() {
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA).setVersion(0).build());
+        assertThat(exception).hasMessageThat().contains("Version must be a positive number");
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).setVersion(1).build();
+        assertThat(request.getVersion()).isEqualTo(1);
+    }
+
+    @Test
+    public void testSetVersion_emptyDb() {
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder().setVersion(135).build());
+        assertThat(exception).hasMessageThat().contains(
+                "Cannot set version to the request if schema is empty.");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaResponseCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaResponseCtsTest.java
new file mode 100644
index 0000000..667658f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaResponseCtsTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.SetSchemaResponse;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class SetSchemaResponseCtsTest {
+    @Test
+    public void testRebuild() {
+        SetSchemaResponse.MigrationFailure failure1 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure1",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"));
+        SetSchemaResponse.MigrationFailure failure2 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure2",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"));
+
+        SetSchemaResponse.Builder builder = new SetSchemaResponse.Builder()
+                .addDeletedType("delete1")
+                .addIncompatibleType("incompatible1")
+                .addMigratedType("migrated1")
+                .addMigrationFailure(failure1);
+        SetSchemaResponse original = builder.build();
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        SetSchemaResponse rebuild = builder
+                .addDeletedType("delete2")
+                .addIncompatibleType("incompatible2")
+                .addMigratedType("migrated2")
+                .addMigrationFailure(failure2)
+                .build();
+
+        // rebuild won't effect the original object
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        assertThat(rebuild.getDeletedTypes()).containsExactly("delete1", "delete2");
+        assertThat(rebuild.getIncompatibleTypes()).containsExactly("incompatible1",
+                "incompatible2");
+        assertThat(rebuild.getMigratedTypes()).containsExactly("migrated1", "migrated2");
+        assertThat(rebuild.getMigrationFailures()).containsExactly(failure1, failure2);
+    }
+
+    @Test
+    public void testPluralAdds() {
+        SetSchemaResponse.MigrationFailure failure1 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure1",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"));
+
+        SetSchemaResponse.Builder builder = new SetSchemaResponse.Builder()
+                .addDeletedTypes(Arrays.asList("delete1"))
+                .addIncompatibleTypes(Arrays.asList("incompatible1"))
+                .addMigratedTypes(Arrays.asList("migrated1"))
+                .addMigrationFailures(Arrays.asList(failure1));
+        SetSchemaResponse singleEntries = builder.build();
+        assertThat(singleEntries.getDeletedTypes()).containsExactly("delete1");
+        assertThat(singleEntries.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(singleEntries.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(singleEntries.getMigrationFailures()).containsExactly(failure1);
+
+        SetSchemaResponse.MigrationFailure failure2 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure2",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"));
+        SetSchemaResponse multiEntries = builder
+                .addDeletedTypes(Arrays.asList("delete2", "deleted3", "deleted4"))
+                .addIncompatibleTypes(Arrays.asList("incompatible2"))
+                .addMigratedTypes(Arrays.asList("migrated2", "migrate3"))
+                .addMigrationFailures(Arrays.asList(failure2))
+                .build();
+
+        assertThat(multiEntries.getDeletedTypes()).containsExactly("delete1", "delete2", "deleted3",
+                "deleted4");
+        assertThat(multiEntries.getIncompatibleTypes()).containsExactly("incompatible1",
+                "incompatible2");
+        assertThat(multiEntries.getMigratedTypes()).containsExactly("migrated1", "migrated2",
+                "migrate3");
+        assertThat(multiEntries.getMigrationFailures()).containsExactly(failure1, failure2);
+    }
+
+    @Test
+    public void testMigrationFailure() {
+        AppSearchResult<Void> expectedResult = AppSearchResult.newFailedResult(
+                AppSearchResult.RESULT_INTERNAL_ERROR, "This is errorMessage.");
+        SetSchemaResponse.MigrationFailure migrationFailure =
+                new SetSchemaResponse.MigrationFailure("testNamespace", "testId",
+                        "testSchemaType", expectedResult);
+        assertThat(migrationFailure.getNamespace()).isEqualTo("testNamespace");
+        assertThat(migrationFailure.getSchemaType()).isEqualTo("testSchemaType");
+        assertThat(migrationFailure.getDocumentId()).isEqualTo("testId");
+        assertThat(migrationFailure.getAppSearchResult()).isEqualTo(expectedResult);
+        assertThat(migrationFailure.toString()).isEqualTo("MigrationFailure { schemaType:"
+                + " testSchemaType, namespace: testNamespace, documentId: testId, "
+                + "appSearchResult: [FAILURE(2)]: This is errorMessage.}");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/StorageInfoCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/StorageInfoCtsTest.java
new file mode 100644
index 0000000..f4da5e3
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/StorageInfoCtsTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.StorageInfo;
+
+import org.junit.Test;
+
+public class StorageInfoCtsTest {
+
+    @Test
+    public void testBuildStorageInfo() {
+        StorageInfo storageInfo =
+                new StorageInfo.Builder()
+                        .setAliveDocumentsCount(10)
+                        .setSizeBytes(1L)
+                        .setAliveNamespacesCount(10)
+                        .build();
+
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(10);
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(1L);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(10);
+    }
+
+    @Test
+    public void testBuildStorageInfo_withDefaults() {
+        StorageInfo storageInfo = new StorageInfo.Builder().build();
+
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0L);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/CustomerDocumentTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/customer/CustomerDocumentTest.java
similarity index 87%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/CustomerDocumentTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/customer/CustomerDocumentTest.java
index 07297fb..c192d96 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/CustomerDocumentTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/customer/CustomerDocumentTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.appsearch.app.cts.customer;
+package androidx.appsearch.cts.app.customer;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -35,15 +35,15 @@
     private static final byte[] BYTE_ARRAY1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
     private static final byte[] BYTE_ARRAY2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};
     private static final GenericDocument DOCUMENT_PROPERTIES1 = new GenericDocument
-            .Builder<>("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+            .Builder<>("namespace", "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
             .build();
     private static final GenericDocument DOCUMENT_PROPERTIES2 = new GenericDocument
-            .Builder<>("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+            .Builder<>("namespace", "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
             .build();
 
     @Test
     public void testBuildCustomerDocument() {
-        CustomerDocument customerDocument = new CustomerDocument.Builder("uri1")
+        CustomerDocument customerDocument = new CustomerDocument.Builder("namespace", "id1")
                 .setScore(1)
                 .setCreationTimestampMillis(0)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
@@ -54,7 +54,8 @@
                 .setPropertyDocument("documentKey1", DOCUMENT_PROPERTIES1, DOCUMENT_PROPERTIES2)
                 .build();
 
-        assertThat(customerDocument.getUri()).isEqualTo("uri1");
+        assertThat(customerDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(customerDocument.getId()).isEqualTo("id1");
         assertThat(customerDocument.getSchemaType()).isEqualTo("customerDocument");
         assertThat(customerDocument.getScore()).isEqualTo(1);
         assertThat(customerDocument.getCreationTimestampMillis()).isEqualTo(0L);
@@ -83,8 +84,8 @@
         }
 
         public static class Builder extends GenericDocument.Builder<CustomerDocument.Builder> {
-            private Builder(@NonNull String uri) {
-                super(uri, "customerDocument");
+            private Builder(@NonNull String namespace, @NonNull String id) {
+                super(namespace, id, "customerDocument");
             }
 
             @Override
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/customer/EmailDocument.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/customer/EmailDocument.java
new file mode 100644
index 0000000..428781e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/customer/EmailDocument.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.cts.app.customer;
+
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+
+@Document
+public final class EmailDocument {
+    @Document.Namespace
+    public String namespace;
+
+    @Document.Id
+    public String id;
+
+    @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+    public String subject;
+
+    @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+    public String body;
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/exceptions/AppSearchExceptionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/exceptions/AppSearchExceptionCtsTest.java
new file mode 100644
index 0000000..a732033
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/exceptions/AppSearchExceptionCtsTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.cts.exceptions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.exceptions.AppSearchException;
+
+import org.junit.Test;
+
+public class AppSearchExceptionCtsTest {
+    @Test
+    public void testNoMessageException() {
+        AppSearchException e = new AppSearchException(AppSearchResult.RESULT_IO_ERROR);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_IO_ERROR);
+
+        AppSearchResult<?> result = e.toAppSearchResult();
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_IO_ERROR);
+        assertThat(result.getErrorMessage()).isNull();
+    }
+
+    @Test
+    public void testExceptionWithMessage() {
+        AppSearchException e =
+                new AppSearchException(AppSearchResult.RESULT_NOT_FOUND, "ERROR!");
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        AppSearchResult<?> result = e.toAppSearchResult();
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(result.getErrorMessage()).isEqualTo("ERROR!");
+    }
+
+    @Test
+    public void testExceptionWithThrowable() {
+        IllegalArgumentException throwable = new IllegalArgumentException("You can't do that!");
+        AppSearchException e = new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT,
+                "ERROR!", throwable);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_INVALID_ARGUMENT);
+        assertThat(e.getCause()).isEqualTo(throwable);
+
+        AppSearchResult<?> result = e.toAppSearchResult();
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_INVALID_ARGUMENT);
+        assertThat(result.getErrorMessage()).isEqualTo("ERROR!");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/exceptions/IllegalSchemaExceptionTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/exceptions/IllegalSchemaExceptionTest.java
new file mode 100644
index 0000000..67a1234
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/exceptions/IllegalSchemaExceptionTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.exceptions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class IllegalSchemaExceptionTest {
+    @Test
+    public void testExceptionWithMessage() {
+        IllegalSchemaException e = new IllegalSchemaException("ERROR MESSAGE");
+        assertThat(e.getMessage()).isEqualTo("ERROR MESSAGE");
+        assertThat(e).isInstanceOf(IllegalArgumentException.class);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
index 389c3ee..55b7638 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
@@ -201,6 +201,29 @@
         assertThat(BundleUtil.deepHashCode(b1)).isNotEqualTo(BundleUtil.deepHashCode(b2));
     }
 
+    @Test
+    public void testDeepHashCode_differentKeys() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            Bundle b = new Bundle();
+            b.putString("key" + i, "value");
+            inputs[i] = b;
+        }
+        assertThat(BundleUtil.deepHashCode(inputs[0]))
+                .isNotEqualTo(BundleUtil.deepHashCode(inputs[1]));
+    }
+
+    @Test
+    public void testDeepCopy() {
+        Bundle input = createThoroughBundle();
+        Bundle output = BundleUtil.deepCopy(input);
+        assertThat(input).isNotSameInstanceAs(output);
+        assertThat(BundleUtil.deepEquals(input, output)).isTrue();
+
+        output.getIntegerArrayList("integerArrayList").add(5);
+        assertThat(BundleUtil.deepEquals(input, output)).isFalse();
+    }
+
     private static Bundle createThoroughBundle() {
         Bundle toy1 = new Bundle();
         toy1.putString("a", "a");
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/IndentingStringBuilderTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/IndentingStringBuilderTest.java
new file mode 100644
index 0000000..28ad13e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/IndentingStringBuilderTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+public class IndentingStringBuilderTest {
+    @Test
+    public void testAppendIndentedStrings() {
+        IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+        stringBuilder
+                .increaseIndentLevel()
+                .append("\nIndentLevel1\nIndentLevel1\n")
+                .decreaseIndentLevel()
+                .append("IndentLevel0,\n");
+
+        String str = stringBuilder.toString();
+        String expectedString = "\n  IndentLevel1\n  IndentLevel1\nIndentLevel0,\n";
+
+        assertThat(str).isEqualTo(expectedString);
+    }
+
+    @Test
+    public void testDecreaseIndentLevel_throwsException() {
+        IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+
+        Exception e = assertThrows(IllegalStateException.class,
+                () -> stringBuilder.decreaseIndentLevel());
+        assertThat(e).hasMessageThat().contains("Cannot set indent level below 0.");
+    }
+
+    @Test
+    public void testAppendIndentedObjects() {
+        IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+        Object stringProperty = "String";
+        Object longProperty = 1L;
+        Object booleanProperty = true;
+
+        stringBuilder
+                .append(stringProperty)
+                .append("\n")
+                .increaseIndentLevel()
+                .append(longProperty)
+                .append("\n")
+                .decreaseIndentLevel()
+                .append(booleanProperty);
+
+        String str = stringBuilder.toString();
+        String expectedString = "String\n  1\ntrue";
+
+        assertThat(str).isEqualTo(expectedString);
+    }
+
+    @Test
+    public void testAppendIndentedStrings_doesNotIndentLineBreak() {
+        IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+
+        stringBuilder
+                .append("\n")
+                .increaseIndentLevel()
+                .append("\n\n")
+                .decreaseIndentLevel()
+                .append("\n");
+
+        String str = stringBuilder.toString();
+        String expectedString = "\n\n\n\n";
+
+        assertThat(str).isEqualTo(expectedString);
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
deleted file mode 100644
index a4243eb..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * 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.
- */
-// @exportToFramework:skipFile()
-package androidx.appsearch.annotation;
-
-import androidx.appsearch.app.AppSearchSchema;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Marks a class as a data class known to AppSearch.
- *
- * <p>Each field annotated with {@link Property @Property} will become an AppSearch searchable
- * property. Fields annotated with other annotations included here (like {@link Uri @Uri}) will have
- * the special behaviour described in that annotation. All other members (those which do not have
- * any of these annotations) will be ignored by AppSearch and will not be persisted or set.
- *
- * <p>Each AppSearch field, whether marked by {@link Property @Property} or by one of the other
- * annotations here, must meet at least one the following conditions:
- * <ol>
- *     <li>There must be a getter named get&lt;Fieldname&gt; in the class (with package-private
- *     visibility or greater), or
- *     <li>The field itself must have package-private visibility or greater.
- * </ol>
- *
- * <p>The field must also meet at least one of the following conditions:
- * <ol>
- *     <li>There must be a setter named set&lt;Fieldname&gt; in the class (with package-private
- *     visibility or greater), or
- *     <li>The field itself must be mutable (non-final) and have package-private visibility or
- *     greater, or
- *     <li>There must be a constructor that accepts all fields not meeting condition 1. and 2. as
- *     parameters. That constructor must have package-private visibility or greater. It may
- *     also accept fields that do meet conditions 1 and 2, in which case the constructor will be
- *     used to populate those fields instead of methods 1 and 2.
- * </ol>
- *
- * <p>The class must also have exactly one member annotated with {@link Uri @Uri}.
- */
-@Documented
-@Retention(RetentionPolicy.CLASS)
-@Target(ElementType.TYPE)
-public @interface AppSearchDocument {
-    /**
-     * Marks a member field of a document as the document's URI.
-     *
-     * <p>Indexing a document with a particular {@link java.net.URI} replaces any existing
-     * documents with the same URI in that namespace.
-     *
-     * <p>A document must have exactly one such field, and it must be of type {@link String} or
-     * {@link android.net.Uri}.
-     *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. it
-     * must be visible, or have a visible getter and setter, or be exposed through a visible
-     * constructor).
-     */
-    @Documented
-    @Retention(RetentionPolicy.CLASS)
-    @Target(ElementType.FIELD)
-    @interface Uri {}
-
-    /**
-     * Marks a member field of a document as the document's namespace.
-     *
-     * <p>The namespace is an arbitrary user-provided string that can be used to group documents
-     * during querying or deletion. Indexing a document with a particular {@link java.net.URI}
-     * replaces any existing documents with the same URI in that namespace.
-     *
-     * <p>This field is not required. If not present or not set, the document will be assigned to
-     * the default namespace, {@link androidx.appsearch.app.GenericDocument#DEFAULT_NAMESPACE}.
-     *
-     * <p>If present, the field must be of type {@code String}.
-     *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
-     * present it must be visible, or have a visible getter and setter, or be exposed through a
-     * visible constructor).
-     */
-    @Documented
-    @Retention(RetentionPolicy.CLASS)
-    @Target(ElementType.FIELD)
-    @interface Namespace {}
-
-    /**
-     * Marks a member field of a document as the document's creation timestamp.
-     *
-     * <p>The creation timestamp is used for document expiry (see {@link TtlMillis}) and as one
-     * of the sort options for queries.
-     *
-     * <p>This field is not required. If not present or not set, the document will be assigned
-     * the current timestamp as its creation timestamp.
-     *
-     * <p>If present, the field must be of type {@code long} or {@link Long}.
-     *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
-     * present it must be visible, or have a visible getter and setter, or be exposed through a
-     * visible constructor).
-     */
-    @Documented
-    @Retention(RetentionPolicy.CLASS)
-    @Target(ElementType.FIELD)
-    @interface CreationTimestampMillis {}
-
-    /**
-     * Marks a member field of a document as the document's time-to-live (TTL).
-     *
-     * <p>The document will be automatically deleted {@link TtlMillis} milliseconds after
-     * {@link CreationTimestampMillis}.
-     *
-     * <p>This field is not required. If not present or not set, the document will never expire.
-     *
-     * <p>If present, the field must be of type {@code long} or {@link Long}.
-     *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
-     * present it must be visible, or have a visible getter and setter, or be exposed through a
-     * visible constructor).
-     */
-    @Documented
-    @Retention(RetentionPolicy.CLASS)
-    @Target(ElementType.FIELD)
-    @interface TtlMillis {}
-
-    /**
-     * Marks a member field of a document as the document's query-independent score.
-     *
-     * <p>The score is a query-independent measure of the document's quality, relative to other
-     * documents of the same type. It is one of the sort options for queries.
-     *
-     * <p>This field is not required. If not present or not set, the document will have a score
-     * of 0.
-     *
-     * <p>If present, the field must be of type {@code int} or {@link Integer}.
-     *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
-     * present it must be visible, or have a visible getter and setter, or be exposed through a
-     * visible constructor).
-     */
-    @Documented
-    @Retention(RetentionPolicy.CLASS)
-    @Target(ElementType.FIELD)
-    @interface Score {}
-
-    /**
-     * Configures a member field of a class as a property known to AppSearch.
-     *
-     * <p>Properties contain the document's data. They may be indexed or non-indexed (the default).
-     * Only indexed properties can be searched for in queries. There is a limit of
-     * {@link androidx.appsearch.app.GenericDocument#getMaxIndexedProperties} indexed properties in
-     * one document.
-     */
-    @Documented
-    @Retention(RetentionPolicy.CLASS)
-    @Target(ElementType.FIELD)
-    @interface Property {
-        /**
-         * The name of this property. This string is used to query against this property.
-         *
-         * <p>If not specified, the name of the field in the code will be used instead.
-         */
-        String name() default "";
-
-        /**
-         * Configures how tokens should be extracted from this property.
-         *
-         * <p>If not specified, defaults to {@link
-         * AppSearchSchema.PropertyConfig#TOKENIZER_TYPE_PLAIN} (the field will be tokenized
-         * along word boundaries as plain text).
-         */
-        @AppSearchSchema.PropertyConfig.TokenizerType int tokenizerType()
-                default AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
-
-        /**
-         * Configures how a property should be indexed so that it can be retrieved by queries.
-         *
-         * <p>If not specified, defaults to {@link
-         * AppSearchSchema.PropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed and
-         * cannot be queried).
-         * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config.
-         */
-        @AppSearchSchema.PropertyConfig.IndexingType int indexingType()
-                default AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
-
-        /**
-         * Configures whether this property must be specified for the document to be valid.
-         *
-         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
-         *
-         * <p>Please make sure you understand the consequences of required fields on
-         * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
-         * this attribute to {@code true}.
-         */
-        boolean required() default false;
-    }
-
-    /**
-     * The schema name of this type.
-     *
-     * <p>This string is the key to which the complete schema definition is associated in the
-     * AppSearch database. It can be specified to replace an existing type with a new definition.
-     *
-     * <p>If not specified, it will be automatically set to the simple name of the annotated class.
-     */
-    String name() default "";
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
new file mode 100644
index 0000000..527127f
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -0,0 +1,363 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.annotation;
+
+import androidx.appsearch.app.AppSearchSchema;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class as an entity known to AppSearch containing a data record.
+ *
+ * <p>Each field annotated with one of the Property annotations will become an AppSearch searchable
+ * property. Fields annotated with other annotations included here (like {@link Id @Id}) will have
+ * the special behaviour described in that annotation. All other members (those which do not have
+ * any of these annotations) will be ignored by AppSearch and will not be persisted or set.
+ *
+ * <p>Each AppSearch annotated field must meet at least one the following conditions:
+ * <ol>
+ *     <li>There must be a getter named get&lt;Fieldname&gt; in the class (with package-private
+ *     visibility or greater), or
+ *     <li>The field itself must have package-private visibility or greater.
+ * </ol>
+ *
+ * <p>The field must also meet at least one of the following conditions:
+ * <ol>
+ *     <li>There must be a setter named {@code set<FieldName>(arg)} in the class (with
+ *     package-private visibility or greater), or
+ *     <li>There must be a setter named {@code fieldname(arg)} in the class (with package-private
+ *     visibility or greater), or
+ *     <li>The field itself must be mutable (non-final) and have package-private visibility or
+ *     greater, or
+ *     <li>There must be a constructor that accepts all fields not meeting condition 1. and 2. as
+ *     parameters. That constructor must have package-private visibility or greater. It may
+ *     also accept fields that do meet conditions 1 and 2, in which case the constructor will be
+ *     used to populate those fields instead of methods 1 and 2.
+ * </ol>
+ *
+ * <p>Fields may be named according to any of the following conventions:
+ * <ul>
+ *   <li>exampleName
+ *   <li>mExampleName
+ *   <li>_exampleName
+ *   <li>exampleName_
+ * </ul>
+ *
+ * <p>In all of the above cases, the default property name will be "exampleName", the allowed
+ * getters are {@code getExampleName()} or {@code exampleName()}, the allowed setters are {@code
+ * setExampleName(arg)} or {@code exampleName(arg)}, and the expected constructor parameter for
+ * the field is "exampleName".
+ *
+ * <p>The class must also have exactly one member annotated with {@link Id @Id}.
+ *
+ * <p>Properties contain the document's data. They may be indexed or non-indexed (the default).
+ * Only indexed properties can be searched for in queries. There is a limit of
+ * {@link androidx.appsearch.app.GenericDocument#getMaxIndexedProperties} indexed properties in
+ * one document.
+ */
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target(ElementType.TYPE)
+public @interface Document {
+    /**
+     * The schema name of this type.
+     *
+     * <p>This string is the key to which the complete schema definition is associated in the
+     * AppSearch database. It can be specified to replace an existing type with a new definition.
+     *
+     * <p>If not specified, it will be automatically set to the simple name of the annotated class.
+     */
+    String name() default "";
+
+    /**
+     * Marks a member field of a document as the document's unique identifier (ID).
+     *
+     * <p>Indexing a document with a particular ID replaces any existing documents with the same
+     * ID in that namespace.
+     *
+     * <p>A document must have exactly one such field, and it must be of type {@link String}.
+     *
+     * <p>See the class description of {@link Document} for other requirements (i.e. it
+     * must be visible, or have a visible getter and setter, or be exposed through a visible
+     * constructor).
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface Id {}
+
+    /**
+     * Marks a member field of a document as the document's namespace.
+     *
+     * <p>The namespace is an arbitrary user-provided string that can be used to group documents
+     * during querying or deletion. Indexing a document with a particular ID replaces any existing
+     * documents with the same ID in that namespace.
+     *
+     * <p>A document must have exactly one such field, and it must be of type {@link String}.
+     *
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
+     * present it must be visible, or have a visible getter and setter, or be exposed through a
+     * visible constructor).
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface Namespace {}
+
+    /**
+     * Marks a member field of a document as the document's creation timestamp.
+     *
+     * <p>The creation timestamp is used for document expiry (see {@link TtlMillis}) and as one
+     * of the sort options for queries.
+     *
+     * <p>This field is not required. If not present or not set, the document will be assigned
+     * the current timestamp as its creation timestamp.
+     *
+     * <p>If present, the field must be of type {@code long} or {@link Long}.
+     *
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
+     * present it must be visible, or have a visible getter and setter, or be exposed through a
+     * visible constructor).
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface CreationTimestampMillis {}
+
+    /**
+     * Marks a member field of a document as the document's time-to-live (TTL).
+     *
+     * <p>The document will be automatically deleted {@link TtlMillis} milliseconds after
+     * {@link CreationTimestampMillis}.
+     *
+     * <p>This field is not required. If not present or not set, the document will never expire.
+     *
+     * <p>If present, the field must be of type {@code long} or {@link Long}.
+     *
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
+     * present it must be visible, or have a visible getter and setter, or be exposed through a
+     * visible constructor).
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface TtlMillis {}
+
+    /**
+     * Marks a member field of a document as the document's query-independent score.
+     *
+     * <p>The score is a query-independent measure of the document's quality, relative to other
+     * documents of the same type. It is one of the sort options for queries.
+     *
+     * <p>This field is not required. If not present or not set, the document will have a score
+     * of 0.
+     *
+     * <p>If present, the field must be of type {@code int} or {@link Integer}.
+     *
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
+     * present it must be visible, or have a visible getter and setter, or be exposed through a
+     * visible constructor).
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface Score {}
+
+    /** Configures a string member field of a class as a property known to AppSearch. */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface StringProperty {
+        /**
+         * The name of this property. This string is used to query against this property.
+         *
+         * <p>If not specified, the name of the field in the code will be used instead.
+         */
+        String name() default "";
+
+        /**
+         * Configures how tokens should be extracted from this property.
+         *
+         * <p>If not specified, defaults to {@link
+         * AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_PLAIN} (the field will be tokenized
+         * along word boundaries as plain text).
+         */
+        @AppSearchSchema.StringPropertyConfig.TokenizerType int tokenizerType()
+                default AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
+
+        /**
+         * Configures how a property should be indexed so that it can be retrieved by queries.
+         *
+         * <p>If not specified, defaults to {@link
+         * AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed
+         * and cannot be queried).
+         * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config.
+         */
+        @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType()
+                default AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
+
+        /**
+         * Configures whether this property must be specified for the document to be valid.
+         *
+         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
+         *
+         * <p>Please make sure you understand the consequences of required fields on
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+         * this attribute to {@code true}.
+         */
+        boolean required() default false;
+    }
+
+    /**
+     * Configures a member field of a class as a property known to AppSearch.
+     *
+     * <p>Field's data class is required to be annotated with {@link Document}.
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface DocumentProperty {
+        /**
+         * The name of this property. This string is used to query against this property.
+         *
+         * <p>If not specified, the name of the field in the code will be used instead.
+         */
+        String name() default "";
+
+        /**
+         * Configures whether fields in the nested document should be indexed.
+         *
+         * <p>If false, the nested document's properties are not indexed regardless of its own
+         * schema.
+         */
+        boolean indexNestedProperties() default false;
+
+        /**
+         * Configures whether this property must be specified for the document to be valid.
+         *
+         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
+         *
+         * <p>Please make sure you understand the consequences of required fields on
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+         * this attribute to {@code true}.
+         */
+        boolean required() default false;
+    }
+
+    /** Configures a 64-bit integer field of a class as a property known to AppSearch. */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface LongProperty {
+        /**
+         * The name of this property. This string is used to query against this property.
+         *
+         * <p>If not specified, the name of the field in the code will be used instead.
+         */
+        String name() default "";
+
+        /**
+         * Configures whether this property must be specified for the document to be valid.
+         *
+         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
+         *
+         * <p>Please make sure you understand the consequences of required fields on
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+         * this attribute to {@code true}.
+         */
+        boolean required() default false;
+    }
+
+    /**
+     * Configures a double-precision decimal number field of a class as a property known to
+     * AppSearch.
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface DoubleProperty {
+        /**
+         * The name of this property. This string is used to query against this property.
+         *
+         * <p>If not specified, the name of the field in the code will be used instead.
+         */
+        String name() default "";
+
+        /**
+         * Configures whether this property must be specified for the document to be valid.
+         *
+         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
+         *
+         * <p>Please make sure you understand the consequences of required fields on
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+         * this attribute to {@code true}.
+         */
+        boolean required() default false;
+    }
+
+    /** Configures a boolean member field of a class as a property known to AppSearch. */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface BooleanProperty {
+        /**
+         * The name of this property. This string is used to query against this property.
+         *
+         * <p>If not specified, the name of the field in the code will be used instead.
+         */
+        String name() default "";
+
+        /**
+         * Configures whether this property must be specified for the document to be valid.
+         *
+         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
+         *
+         * <p>Please make sure you understand the consequences of required fields on
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+         * this attribute to {@code true}.
+         */
+        boolean required() default false;
+    }
+
+    /** Configures a byte array member field of a class as a property known to AppSearch. */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface BytesProperty {
+        /**
+         * The name of this property. This string is used to query against this property.
+         *
+         * <p>If not specified, the name of the field in the code will be used instead.
+         */
+        String name() default "";
+
+        /**
+         * Configures whether this property must be specified for the document to be valid.
+         *
+         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
+         *
+         * <p>Please make sure you understand the consequences of required fields on
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema schema migration} before setting
+         * this attribute to {@code true}.
+         */
+        boolean required() default false;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
index 302545b..4b6ee7c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
 import androidx.annotation.NonNull;
@@ -21,24 +20,39 @@
 import androidx.collection.ArrayMap;
 import androidx.core.util.Preconditions;
 
+import java.util.Collections;
 import java.util.Map;
 
 /**
- * Provides access to multiple {@link AppSearchResult}s from a batch operation accepting multiple
- * inputs.
+ * Provides results for AppSearch batch operations which encompass multiple documents.
  *
- * @param <KeyType> The type of the keys for {@link #getSuccesses} and {@link #getFailures}.
- * @param <ValueType> The type of result objects associated with the keys.
+ * <p>Individual results of a batch operation are separated into two maps: one for successes and
+ * one for failures. For successes, {@link #getSuccesses()} will return a map of keys to
+ * instances of the value type. For failures, {@link #getFailures()} will return a map of keys to
+ * {@link AppSearchResult} objects.
+ *
+ * <p>Alternatively, {@link #getAll()} returns a map of keys to {@link AppSearchResult} objects for
+ * both successes and failures.
+ *
+ * @param <KeyType> The type of the keys for which the results will be reported.
+ * @param <ValueType> The type of the result objects for successful results.
+ *
+ * @see AppSearchSession#put
+ * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#remove
  */
 public final class AppSearchBatchResult<KeyType, ValueType> {
     @NonNull private final Map<KeyType, ValueType> mSuccesses;
     @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;
+    @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mAll;
 
     AppSearchBatchResult(
             @NonNull Map<KeyType, ValueType> successes,
-            @NonNull Map<KeyType, AppSearchResult<ValueType>> failures) {
-        mSuccesses = successes;
-        mFailures = failures;
+            @NonNull Map<KeyType, AppSearchResult<ValueType>> failures,
+            @NonNull Map<KeyType, AppSearchResult<ValueType>> all) {
+        mSuccesses = Preconditions.checkNotNull(successes);
+        mFailures = Preconditions.checkNotNull(failures);
+        mAll = Preconditions.checkNotNull(all);
     }
 
     /** Returns {@code true} if this {@link AppSearchBatchResult} has no failures. */
@@ -47,25 +61,40 @@
     }
 
     /**
-     * Returns a {@link Map} of all successful keys mapped to the successful
-     * {@link AppSearchResult}s they produced.
+     * Returns a {@link Map} of keys mapped to instances of the value type for all successful
+     * individual results.
+     *
+     * <p>Example: {@link AppSearchSession#getByDocumentId} returns an {@link AppSearchBatchResult}.
+     * Each key (the document ID, of {@code String} type) will map to a {@link GenericDocument}
+     * object.
      *
      * <p>The values of the {@link Map} will not be {@code null}.
      */
     @NonNull
     public Map<KeyType, ValueType> getSuccesses() {
-        return mSuccesses;
+        return Collections.unmodifiableMap(mSuccesses);
     }
 
     /**
-     * Returns a {@link Map} of all failed keys mapped to the failed {@link AppSearchResult}s they
-     * produced.
+     * Returns a {@link Map} of keys mapped to instances of {@link AppSearchResult} for all
+     * failed individual results.
      *
      * <p>The values of the {@link Map} will not be {@code null}.
      */
     @NonNull
     public Map<KeyType, AppSearchResult<ValueType>> getFailures() {
-        return mFailures;
+        return Collections.unmodifiableMap(mFailures);
+    }
+
+    /**
+     * Returns a {@link Map} of keys mapped to instances of {@link AppSearchResult} for all
+     * individual results.
+     *
+     * <p>The values of the {@link Map} will not be {@code null}.
+     */
+    @NonNull
+    public Map<KeyType, AppSearchResult<ValueType>> getAll() {
+        return Collections.unmodifiableMap(mAll);
     }
 
     /**
@@ -87,54 +116,78 @@
     /**
      * Builder for {@link AppSearchBatchResult} objects.
      *
-     * @param <KeyType> The type of keys.
-     * @param <ValueType> The type of result objects associated with the keys.
-     * @hide
+     * @param <KeyType> The type of the keys for which the results will be reported.
+     * @param <ValueType> The type of the result objects for successful results.
      */
     public static final class Builder<KeyType, ValueType> {
-        private final Map<KeyType, ValueType> mSuccesses = new ArrayMap<>();
-        private final Map<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
+        private ArrayMap<KeyType, ValueType> mSuccesses = new ArrayMap<>();
+        private ArrayMap<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
+        private ArrayMap<KeyType, AppSearchResult<ValueType>> mAll = new ArrayMap<>();
         private boolean mBuilt = false;
 
         /**
-         * Associates the {@code key} with the given successful return value.
+         * Associates the {@code key} with the provided successful return value.
          *
          * <p>Any previous mapping for a key, whether success or failure, is deleted.
+         *
+         * <p>This is a convenience function which is equivalent to
+         * {@code setResult(key, AppSearchResult.newSuccessfulResult(value))}.
+         *
+         * @param key   The key to associate the result with; usually corresponds to some
+         *              identifier from the input like an ID or name.
+         * @param value An optional value to associate with the successful result of the operation
+         *              being performed.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")  // See getSuccesses
         @NonNull
         public Builder<KeyType, ValueType> setSuccess(
-                @NonNull KeyType key, @Nullable ValueType result) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
+                @NonNull KeyType key, @Nullable ValueType value) {
             Preconditions.checkNotNull(key);
-            return setResult(key, AppSearchResult.newSuccessfulResult(result));
+            resetIfBuilt();
+            return setResult(key, AppSearchResult.newSuccessfulResult(value));
         }
 
         /**
-         * Associates the {@code key} with the given failure code and error message.
+         * Associates the {@code key} with the provided failure code and error message.
          *
          * <p>Any previous mapping for a key, whether success or failure, is deleted.
+         *
+         * <p>This is a convenience function which is equivalent to
+         * {@code setResult(key, AppSearchResult.newFailedResult(resultCode, errorMessage))}.
+         *
+         * @param key          The key to associate the result with; usually corresponds to some
+         *                     identifier from the input like an ID or name.
+         * @param resultCode   One of the constants documented in
+         *                     {@link AppSearchResult#getResultCode}.
+         * @param errorMessage An optional string describing the reason or nature of the failure.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")  // See getFailures
         @NonNull
         public Builder<KeyType, ValueType> setFailure(
                 @NonNull KeyType key,
                 @AppSearchResult.ResultCode int resultCode,
                 @Nullable String errorMessage) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkNotNull(key);
+            resetIfBuilt();
             return setResult(key, AppSearchResult.newFailedResult(resultCode, errorMessage));
         }
 
         /**
-         * Associates the {@code key} with the given {@code result}.
+         * Associates the {@code key} with the provided {@code result}.
          *
          * <p>Any previous mapping for a key, whether success or failure, is deleted.
+         *
+         * @param key    The key to associate the result with; usually corresponds to some
+         *               identifier from the input like an ID or name.
+         * @param result The result to associate with the key.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")  // See getAll
         @NonNull
         public Builder<KeyType, ValueType> setResult(
                 @NonNull KeyType key, @NonNull AppSearchResult<ValueType> result) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkNotNull(key);
             Preconditions.checkNotNull(result);
+            resetIfBuilt();
             if (result.isSuccess()) {
                 mSuccesses.put(key, result.getResultValue());
                 mFailures.remove(key);
@@ -142,15 +195,26 @@
                 mFailures.put(key, result);
                 mSuccesses.remove(key);
             }
+            mAll.put(key, result);
             return this;
         }
 
-        /** Builds an {@link AppSearchBatchResult} from the contents of this {@link Builder}. */
+        /**
+         * Builds an {@link AppSearchBatchResult} object from the contents of this {@link Builder}.
+         */
         @NonNull
         public AppSearchBatchResult<KeyType, ValueType> build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBuilt = true;
-            return new AppSearchBatchResult<>(mSuccesses, mFailures);
+            return new AppSearchBatchResult<>(mSuccesses, mFailures, mAll);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mSuccesses = new ArrayMap<>(mSuccesses);
+                mFailures = new ArrayMap<>(mFailures);
+                mAll = new ArrayMap<>(mAll);
+                mBuilt = false;
+            }
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
index 75a1053..b204447 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
@@ -13,15 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
+import android.util.Log;
+
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.core.util.ObjectsCompat;
+import androidx.core.util.Preconditions;
 
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -33,6 +35,8 @@
  * @param <ValueType> The type of result object for successful calls.
  */
 public final class AppSearchResult<ValueType> {
+    private static final String TAG = "AppSearchResult";
+
     /**
      * Result codes from {@link AppSearchSession} methods.
      * @hide
@@ -46,6 +50,7 @@
             RESULT_OUT_OF_SPACE,
             RESULT_NOT_FOUND,
             RESULT_INVALID_SCHEMA,
+            RESULT_SECURITY_ERROR,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ResultCode {}
@@ -86,6 +91,9 @@
     /** The caller supplied a schema which is invalid or incompatible with the previous schema. */
     public static final int RESULT_INVALID_SCHEMA = 7;
 
+    /** The caller requested an operation it does not have privileges for. */
+    public static final int RESULT_SECURITY_ERROR = 8;
+
     private final @ResultCode int mResultCode;
     @Nullable private final ValueType mResultValue;
     @Nullable private final String mErrorMessage;
@@ -168,7 +176,9 @@
 
     /**
      * Creates a new successful {@link AppSearchResult}.
-     * @hide
+     *
+     * @param value An optional value to associate with the successful result of the operation
+     *              being performed.
      */
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> newSuccessfulResult(
@@ -178,7 +188,9 @@
 
     /**
      * Creates a new failed {@link AppSearchResult}.
-     * @hide
+     *
+     * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+     * @param errorMessage An optional string describing the reason or nature of the failure.
      */
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> newFailedResult(
@@ -186,25 +198,52 @@
         return new AppSearchResult<>(resultCode, /*resultValue=*/ null, errorMessage);
     }
 
+    /**
+     * Creates a new failed {@link AppSearchResult} by a AppSearchResult in another type.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static <ValueType> AppSearchResult<ValueType> newFailedResult(
+            @NonNull AppSearchResult<?> otherFailedResult) {
+        Preconditions.checkState(!otherFailedResult.isSuccess(),
+                "Cannot convert a success result to a failed result");
+        return AppSearchResult.newFailedResult(
+                otherFailedResult.getResultCode(), otherFailedResult.getErrorMessage());
+    }
+
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> throwableToFailedResult(
             @NonNull Throwable t) {
+        // Log for traceability. NOT_FOUND is logged at VERBOSE because this error can occur during
+        // the regular operation of the system (b/183550974). Everything else is logged at DEBUG.
+        if (t instanceof AppSearchException
+                && ((AppSearchException) t).getResultCode() == RESULT_NOT_FOUND) {
+            Log.v(TAG, "Converting throwable to failed result: " + t);
+        } else {
+            Log.d(TAG, "Converting throwable to failed result.", t);
+        }
+
         if (t instanceof AppSearchException) {
             return ((AppSearchException) t).toAppSearchResult();
         }
 
+        String exceptionClass = t.getClass().getSimpleName();
         @AppSearchResult.ResultCode int resultCode;
-        if (t instanceof IllegalStateException) {
+        if (t instanceof IllegalStateException || t instanceof NullPointerException) {
             resultCode = AppSearchResult.RESULT_INTERNAL_ERROR;
         } else if (t instanceof IllegalArgumentException) {
             resultCode = AppSearchResult.RESULT_INVALID_ARGUMENT;
         } else if (t instanceof IOException) {
             resultCode = AppSearchResult.RESULT_IO_ERROR;
+        } else if (t instanceof SecurityException) {
+            resultCode = AppSearchResult.RESULT_SECURITY_ERROR;
         } else {
             resultCode = AppSearchResult.RESULT_UNKNOWN_ERROR;
         }
-        return AppSearchResult.newFailedResult(resultCode, t.toString());
+        return AppSearchResult.newFailedResult(resultCode, exceptionClass + ": " + t.getMessage());
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index aad7c19..c5c80fce 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -16,7 +16,6 @@
 
 package androidx.appsearch.app;
 
-import android.annotation.SuppressLint;
 import android.os.Bundle;
 
 import androidx.annotation.IntDef;
@@ -25,6 +24,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.IllegalSchemaException;
 import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.util.IndentingStringBuilder;
 import androidx.collection.ArraySet;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
@@ -32,6 +32,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -69,8 +70,45 @@
     }
 
     @Override
+    @NonNull
     public String toString() {
-        return mBundle.toString();
+        IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+        appendAppSearchSchemaString(stringBuilder);
+        return stringBuilder.toString();
+    }
+
+    /**
+     * Appends a debugging string for the {@link AppSearchSchema} instance to the given string
+     * builder.
+     *
+     * @param builder     the builder to append to.
+     */
+    private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) {
+        Preconditions.checkNotNull(builder);
+
+        builder.append("{\n");
+        builder.increaseIndentLevel();
+        builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+        builder.append("properties: [\n");
+
+        AppSearchSchema.PropertyConfig[] sortedProperties = getProperties()
+                .toArray(new AppSearchSchema.PropertyConfig[0]);
+        Arrays.sort(sortedProperties, (o1, o2) -> o1.getName().compareTo(o2.getName()));
+
+        for (int i = 0; i < sortedProperties.length; i++) {
+            AppSearchSchema.PropertyConfig propertyConfig = sortedProperties[i];
+            builder.increaseIndentLevel();
+            propertyConfig.appendPropertyConfigString(builder);
+            if (i != sortedProperties.length - 1) {
+                builder.append(",\n");
+            }
+            builder.decreaseIndentLevel();
+        }
+
+        builder.append("\n");
+        builder.append("]\n");
+        builder.decreaseIndentLevel();
+        builder.append("}");
     }
 
     /** Returns the name of this schema type, e.g. Email. */
@@ -94,7 +132,7 @@
         }
         List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size());
         for (int i = 0; i < propertyBundles.size(); i++) {
-            ret.add(new PropertyConfig(propertyBundles.get(i)));
+            ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));
         }
         return ret;
     }
@@ -122,7 +160,7 @@
     /** Builder for {@link AppSearchSchema objects}. */
     public static final class Builder {
         private final String mSchemaType;
-        private final ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
+        private ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
         private final Set<String> mPropertyNames = new ArraySet<>();
         private boolean mBuilt = false;
 
@@ -133,14 +171,10 @@
         }
 
         /** Adds a property to the given type. */
-        // TODO(b/171360120): MissingGetterMatchingBuilder expects a method called getPropertys, but
-        //  we provide the (correct) method getProperties. Once the bug referenced in this TODO is
-        //  fixed, remove this SuppressLint.
-        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
         public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkNotNull(propertyConfig);
+            resetIfBuilt();
             String name = propertyConfig.getName();
             if (!mPropertyNames.add(name)) {
                 throw new IllegalSchemaException("Property defined more than once: " + name);
@@ -149,35 +183,34 @@
             return this;
         }
 
-        /**
-         * Constructs a new {@link AppSearchSchema} from the contents of this builder.
-         *
-         * <p>After calling this method, the builder must no longer be used.
-         */
+        /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */
         @NonNull
         public AppSearchSchema build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Bundle bundle = new Bundle();
             bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
             bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
             mBuilt = true;
             return new AppSearchSchema(bundle);
         }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mPropertyBundles = new ArrayList<>(mPropertyBundles);
+                mBuilt = false;
+            }
+        }
     }
 
     /**
-     * Configuration for a single property (field) of a document type.
+     * Common configuration for a single property (field) in a Document.
      *
      * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be
      * a property.
      */
-    public static final class PropertyConfig {
-        private static final String NAME_FIELD = "name";
-        private static final String DATA_TYPE_FIELD = "dataType";
-        private static final String SCHEMA_TYPE_FIELD = "schemaType";
-        private static final String CARDINALITY_FIELD = "cardinality";
-        private static final String INDEXING_TYPE_FIELD = "indexingType";
-        private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
+    public abstract static class PropertyConfig {
+        static final String NAME_FIELD = "name";
+        static final String DATA_TYPE_FIELD = "dataType";
+        static final String CARDINALITY_FIELD = "cardinality";
 
         /**
          * Physical data-types of the contents of the property.
@@ -187,7 +220,7 @@
         // com.google.android.icing.proto.PropertyConfigProto.DataType.Code.
         @IntDef(value = {
                 DATA_TYPE_STRING,
-                DATA_TYPE_INT64,
+                DATA_TYPE_LONG,
                 DATA_TYPE_DOUBLE,
                 DATA_TYPE_BOOLEAN,
                 DATA_TYPE_BYTES,
@@ -196,18 +229,33 @@
         @Retention(RetentionPolicy.SOURCE)
         public @interface DataType {}
 
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_STRING = 1;
-        public static final int DATA_TYPE_INT64 = 2;
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public static final int DATA_TYPE_LONG = 2;
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_DOUBLE = 3;
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_BOOLEAN = 4;
 
-        /** Unstructured BLOB. */
+        /**
+         * Unstructured BLOB.
+         * @hide
+         */
         public static final int DATA_TYPE_BYTES = 5;
 
         /**
          * Indicates that the property is itself a {@link GenericDocument}, making it part of a
          * hierarchical schema. Any property using this DataType MUST have a valid
          * {@link PropertyConfig#getSchemaType}.
+         * @hide
          */
         public static final int DATA_TYPE_DOCUMENT = 6;
 
@@ -234,6 +282,166 @@
         /** Exactly one value [1]. */
         public static final int CARDINALITY_REQUIRED = 3;
 
+        final Bundle mBundle;
+
+        @Nullable
+        private Integer mHashCode;
+
+        PropertyConfig(@NonNull Bundle bundle) {
+            mBundle = Preconditions.checkNotNull(bundle);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+            appendPropertyConfigString(stringBuilder);
+            return stringBuilder.toString();
+        }
+
+        /**
+         * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the
+         * given string builder.
+         *
+         * @param builder        the builder to append to.
+         */
+        void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) {
+            Preconditions.checkNotNull(builder);
+
+            builder.append("{\n");
+            builder.increaseIndentLevel();
+            builder.append("name: \"").append(getName()).append("\",\n");
+
+            if (this instanceof AppSearchSchema.StringPropertyConfig) {
+                ((StringPropertyConfig) this)
+                        .appendStringPropertyConfigFields(builder);
+            } else if (this instanceof AppSearchSchema.DocumentPropertyConfig) {
+                ((DocumentPropertyConfig) this)
+                        .appendDocumentPropertyConfigFields(builder);
+            }
+
+            switch (getCardinality()) {
+                case AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED:
+                    builder.append("cardinality: CARDINALITY_REPEATED,\n");
+                    break;
+                case AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL:
+                    builder.append("cardinality: CARDINALITY_OPTIONAL,\n");
+                    break;
+                case AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED:
+                    builder.append("cardinality: CARDINALITY_REQUIRED,\n");
+                    break;
+                default:
+                    builder.append("cardinality: CARDINALITY_UNKNOWN,\n");
+            }
+
+            switch (getDataType()) {
+                case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING:
+                    builder.append("dataType: DATA_TYPE_STRING,\n");
+                    break;
+                case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG:
+                    builder.append("dataType: DATA_TYPE_LONG,\n");
+                    break;
+                case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE:
+                    builder.append("dataType: DATA_TYPE_DOUBLE,\n");
+                    break;
+                case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN:
+                    builder.append("dataType: DATA_TYPE_BOOLEAN,\n");
+                    break;
+                case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES:
+                    builder.append("dataType: DATA_TYPE_BYTES,\n");
+                    break;
+                case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
+                    builder.append("dataType: DATA_TYPE_DOCUMENT,\n");
+                    break;
+                default:
+                    builder.append("dataType: DATA_TYPE_UNKNOWN,\n");
+            }
+            builder.decreaseIndentLevel();
+            builder.append("}");
+        }
+
+        /** Returns the name of this property. */
+        @NonNull
+        public String getName() {
+            return mBundle.getString(NAME_FIELD, "");
+        }
+
+        /**
+         * Returns the type of data the property contains (e.g. string, int, bytes, etc).
+         *
+         * @hide
+         */
+        public @DataType int getDataType() {
+            return mBundle.getInt(DATA_TYPE_FIELD, -1);
+        }
+
+        /**
+         * Returns the cardinality of the property (whether it is optional, required or repeated).
+         */
+        public @Cardinality int getCardinality() {
+            return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof PropertyConfig)) {
+                return false;
+            }
+            PropertyConfig otherProperty = (PropertyConfig) other;
+            return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+        }
+
+        @Override
+        public int hashCode() {
+            if (mHashCode == null) {
+                mHashCode = BundleUtil.deepHashCode(mBundle);
+            }
+            return mHashCode;
+        }
+
+        /**
+         * Converts a {@link Bundle} into a {@link PropertyConfig} depending on its internal data
+         * type.
+         *
+         * <p>The bundle is not cloned.
+         *
+         * @throws IllegalArgumentException if the bundle does no contain a recognized
+         * value in its {@code DATA_TYPE_FIELD}.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {
+            switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {
+                case PropertyConfig.DATA_TYPE_STRING:
+                    return new StringPropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_LONG:
+                    return new LongPropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_DOUBLE:
+                    return new DoublePropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_BOOLEAN:
+                    return new BooleanPropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_BYTES:
+                    return new BytesPropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_DOCUMENT:
+                    return new DocumentPropertyConfig(propertyBundle);
+                default:
+                    throw new IllegalArgumentException(
+                            "Unsupported property bundle of type "
+                                    + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)
+                                    + "; contents: " + propertyBundle);
+            }
+        }
+    }
+
+    /** Configuration for a property of type String in a Document. */
+    public static final class StringPropertyConfig extends PropertyConfig {
+        private static final String INDEXING_TYPE_FIELD = "indexingType";
+        private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
+
         /**
          * Encapsulates the configurations on how AppSearch should query/index these terms.
          * @hide
@@ -246,14 +454,7 @@
         @Retention(RetentionPolicy.SOURCE)
         public @interface IndexingType {}
 
-        /**
-         * Content in this property will not be tokenized or indexed.
-         *
-         * <p>Useful if the data type is not made up of terms (e.g.
-         * {@link PropertyConfig#DATA_TYPE_DOCUMENT} or {@link PropertyConfig#DATA_TYPE_BYTES}
-         * type). None of the properties inside the nested property will be indexed regardless of
-         * the value of {@code indexingType} for the nested properties.
-         */
+        /** Content in this property will not be tokenized or indexed. */
         public static final int INDEXING_TYPE_NONE = 0;
 
         /**
@@ -286,55 +487,28 @@
         public @interface TokenizerType {}
 
         /**
-         * It is only valid for tokenizer_type to be 'NONE' if the data type is
-         * {@link PropertyConfig#DATA_TYPE_DOCUMENT}.
+         * This value indicates that no tokens should be extracted from this property.
+         *
+         * <p>It is only valid for tokenizer_type to be 'NONE' if {@link #getIndexingType} is
+         * {@link #INDEXING_TYPE_NONE}.
          */
         public static final int TOKENIZER_TYPE_NONE = 0;
 
-        /** Tokenization for plain text. */
+        /**
+         * Tokenization for plain text. This value indicates that tokens should be extracted from
+         * this property based on word breaks. Segments of whitespace and punctuation are not
+         * considered tokens.
+         *
+         * <p>Ex. A property with "foo bar. baz." will produce tokens for "foo", "bar" and "baz".
+         * The segments " " and "." will not be considered tokens.
+         *
+         * <p>It is only valid for tokenizer_type to be 'PLAIN' if {@link #getIndexingType} is
+         * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
+         */
         public static final int TOKENIZER_TYPE_PLAIN = 1;
 
-        final Bundle mBundle;
-
-        @Nullable
-        private Integer mHashCode;
-
-        PropertyConfig(@NonNull Bundle bundle) {
-            mBundle = Preconditions.checkNotNull(bundle);
-        }
-
-        @Override
-        public String toString() {
-            return mBundle.toString();
-        }
-
-        /** Returns the name of this property. */
-        @NonNull
-        public String getName() {
-            return mBundle.getString(NAME_FIELD, "");
-        }
-
-        /** Returns the type of data the property contains (e.g. string, int, bytes, etc). */
-        public @DataType int getDataType() {
-            return mBundle.getInt(DATA_TYPE_FIELD, -1);
-        }
-
-        /**
-         * Returns the logical schema-type of the contents of this property.
-         *
-         * <p>Only set when {@link #getDataType} is set to {@link #DATA_TYPE_DOCUMENT}.
-         * Otherwise, it is {@code null}.
-         */
-        @Nullable
-        public String getSchemaType() {
-            return mBundle.getString(SCHEMA_TYPE_FIELD);
-        }
-
-        /**
-         * Returns the cardinality of the property (whether it is optional, required or repeated).
-         */
-        public @Cardinality int getCardinality() {
-            return mBundle.getInt(CARDINALITY_FIELD, -1);
+        StringPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
         }
 
         /** Returns how the property is indexed. */
@@ -347,140 +521,499 @@
             return mBundle.getInt(TOKENIZER_TYPE_FIELD);
         }
 
-        @Override
-        public boolean equals(@Nullable Object other) {
-            if (this == other) {
-                return true;
-            }
-            if (!(other instanceof PropertyConfig)) {
-                return false;
-            }
-            PropertyConfig otherProperty = (PropertyConfig) other;
-            return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
-        }
-
-        @Override
-        public int hashCode() {
-            if (mHashCode == null) {
-                mHashCode = BundleUtil.deepHashCode(mBundle);
-            }
-            return mHashCode;
-        }
-
-        /**
-         * Builder for {@link PropertyConfig}.
-         *
-         * <p>The following properties must be set, or {@link PropertyConfig} construction will
-         * fail:
-         * <ul>
-         *     <li>dataType
-         *     <li>cardinality
-         * </ul>
-         *
-         * <p>In addition, if {@code schemaType} is {@link #DATA_TYPE_DOCUMENT}, {@code schemaType}
-         * is also required.
-         */
+        /** Builder for {@link StringPropertyConfig}. */
         public static final class Builder {
-            private final Bundle mBundle = new Bundle();
-            private boolean mBuilt = false;
+            private final String mPropertyName;
+            private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+            private @IndexingType int mIndexingType = INDEXING_TYPE_NONE;
+            private @TokenizerType int mTokenizerType = TOKENIZER_TYPE_NONE;
 
-            /** Creates a new {@link PropertyConfig.Builder}. */
+            /** Creates a new {@link StringPropertyConfig.Builder}. */
             public Builder(@NonNull String propertyName) {
-                mBundle.putString(NAME_FIELD, propertyName);
+                mPropertyName = Preconditions.checkNotNull(propertyName);
             }
 
             /**
-             * Type of data the property contains (e.g. string, int, bytes, etc).
+             * The cardinality of the property (whether it is optional, required or repeated).
              *
-             * <p>This property must be set.
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
              */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
             @NonNull
-            public PropertyConfig.Builder setDataType(@DataType int dataType) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
+            public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
                 Preconditions.checkArgumentInRange(
-                        dataType, DATA_TYPE_STRING, DATA_TYPE_DOCUMENT, "dataType");
-                mBundle.putInt(DATA_TYPE_FIELD, dataType);
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
                 return this;
             }
 
             /**
-             * The logical schema-type of the contents of this property.
+             * Configures how a property should be indexed so that it can be retrieved by queries.
              *
-             * <p>Only required when {@link #setDataType} is set to
-             * {@link #DATA_TYPE_DOCUMENT}. Otherwise, it is ignored.
+             * <p>If this method is not called, the default indexing type is
+             * {@link StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by
+             * queries.
              */
             @NonNull
-            public PropertyConfig.Builder setSchemaType(@NonNull String schemaType) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                Preconditions.checkNotNull(schemaType);
-                mBundle.putString(SCHEMA_TYPE_FIELD, schemaType);
+            public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+                Preconditions.checkArgumentInRange(
+                        indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
+                mIndexingType = indexingType;
+                return this;
+            }
+
+            /**
+             * Configures how this property should be tokenized (split into words).
+             *
+             * <p>If this method is not called, the default indexing type is
+             * {@link StringPropertyConfig#TOKENIZER_TYPE_NONE}, so that it is not tokenized.
+             *
+             * <p>This method must be called with a value other than
+             * {@link StringPropertyConfig#TOKENIZER_TYPE_NONE} if the property is indexed (i.e.
+             * if {@link #setIndexingType} has been called with a value other than
+             * {@link StringPropertyConfig#INDEXING_TYPE_NONE}).
+             */
+            @NonNull
+            public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
+                Preconditions.checkArgumentInRange(
+                        tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
+                mTokenizerType = tokenizerType;
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link StringPropertyConfig} from the contents of this builder.
+             */
+            @NonNull
+            public StringPropertyConfig build() {
+                if (mTokenizerType == TOKENIZER_TYPE_NONE) {
+                    Preconditions.checkState(mIndexingType == INDEXING_TYPE_NONE, "Cannot set "
+                            + "TOKENIZER_TYPE_NONE with an indexing type other than "
+                            + "INDEXING_TYPE_NONE.");
+                } else {
+                    Preconditions.checkState(mIndexingType != INDEXING_TYPE_NONE, "Cannot set "
+                            + "TOKENIZER_TYPE_PLAIN  with INDEXING_TYPE_NONE.");
+                }
+                Bundle bundle = new Bundle();
+                bundle.putString(NAME_FIELD, mPropertyName);
+                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);
+                bundle.putInt(CARDINALITY_FIELD, mCardinality);
+                bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
+                bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType);
+                return new StringPropertyConfig(bundle);
+            }
+        }
+
+        /**
+         * Appends a debug string for the {@link StringPropertyConfig} instance to the given
+         * string builder.
+         *
+         * <p>This appends fields specific to a {@link StringPropertyConfig} instance.
+         *
+         * @param builder        the builder to append to.
+         */
+        void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
+            switch (getIndexingType()) {
+                case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE:
+                    builder.append("indexingType: INDEXING_TYPE_NONE,\n");
+                    break;
+                case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS:
+                    builder.append("indexingType: INDEXING_TYPE_EXACT_TERMS,\n");
+                    break;
+                case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES:
+                    builder.append("indexingType: INDEXING_TYPE_PREFIXES,\n");
+                    break;
+                default:
+                    builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n");
+            }
+
+            switch (getTokenizerType()) {
+                case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE:
+                    builder.append("tokenizerType: TOKENIZER_TYPE_NONE,\n");
+                    break;
+                case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN:
+                    builder.append("tokenizerType: TOKENIZER_TYPE_PLAIN,\n");
+                    break;
+                default:
+                    builder.append("tokenizerType: TOKENIZER_TYPE_UNKNOWN,\n");
+            }
+        }
+    }
+
+    /**
+     * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Deprecated
+    public static class Int64PropertyConfig extends PropertyConfig {
+        /*@exportToFramework:UnsupportedAppUsage*/
+        Int64PropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link Int64PropertyConfig}. */
+        public static final class Builder {
+            private final String mPropertyName;
+            private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+
+            /** Creates a new {@link Int64PropertyConfig.Builder}. */
+            /*@exportToFramework:UnsupportedAppUsage*/
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            /*@exportToFramework:UnsupportedAppUsage*/
+            public Int64PropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
+                return this;
+            }
+
+            /** Constructs a new {@link Int64PropertyConfig} from the contents of this builder. */
+            @NonNull
+            /*@exportToFramework:UnsupportedAppUsage*/
+            public Int64PropertyConfig build() {
+                Bundle bundle = new Bundle();
+                bundle.putString(NAME_FIELD, mPropertyName);
+                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);
+                bundle.putInt(CARDINALITY_FIELD, mCardinality);
+                return new Int64PropertyConfig(bundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a 64-bit integer. */
+    // TODO(b/181887768): This should extend directly from PropertyConfig
+    public static final class LongPropertyConfig extends Int64PropertyConfig {
+        LongPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link LongPropertyConfig}. */
+        public static final class Builder {
+            private final String mPropertyName;
+            private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+
+            /** Creates a new {@link LongPropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public LongPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
+                return this;
+            }
+
+            /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */
+            @NonNull
+            public LongPropertyConfig build() {
+                Bundle bundle = new Bundle();
+                bundle.putString(NAME_FIELD, mPropertyName);
+                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);
+                bundle.putInt(CARDINALITY_FIELD, mCardinality);
+                return new LongPropertyConfig(bundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a double-precision decimal number. */
+    public static final class DoublePropertyConfig extends PropertyConfig {
+        DoublePropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link DoublePropertyConfig}. */
+        public static final class Builder {
+            private final String mPropertyName;
+            private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+
+            /** Creates a new {@link DoublePropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
+                return this;
+            }
+
+            /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */
+            @NonNull
+            public DoublePropertyConfig build() {
+                Bundle bundle = new Bundle();
+                bundle.putString(NAME_FIELD, mPropertyName);
+                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);
+                bundle.putInt(CARDINALITY_FIELD, mCardinality);
+                return new DoublePropertyConfig(bundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a boolean. */
+    public static final class BooleanPropertyConfig extends PropertyConfig {
+        BooleanPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link BooleanPropertyConfig}. */
+        public static final class Builder {
+            private final String mPropertyName;
+            private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+
+            /** Creates a new {@link BooleanPropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
+                return this;
+            }
+
+            /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */
+            @NonNull
+            public BooleanPropertyConfig build() {
+                Bundle bundle = new Bundle();
+                bundle.putString(NAME_FIELD, mPropertyName);
+                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);
+                bundle.putInt(CARDINALITY_FIELD, mCardinality);
+                return new BooleanPropertyConfig(bundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a byte array. */
+    public static final class BytesPropertyConfig extends PropertyConfig {
+        BytesPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link BytesPropertyConfig}. */
+        public static final class Builder {
+            private final String mPropertyName;
+            private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+
+            /** Creates a new {@link BytesPropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link BytesPropertyConfig} from the contents of this builder.
+             */
+            @NonNull
+            public BytesPropertyConfig build() {
+                Bundle bundle = new Bundle();
+                bundle.putString(NAME_FIELD, mPropertyName);
+                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);
+                bundle.putInt(CARDINALITY_FIELD, mCardinality);
+                return new BytesPropertyConfig(bundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing another Document. */
+    public static final class DocumentPropertyConfig extends PropertyConfig {
+        private static final String SCHEMA_TYPE_FIELD = "schemaType";
+        private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";
+
+        DocumentPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Returns the logical schema-type of the contents of this document property. */
+        @NonNull
+        public String getSchemaType() {
+            return Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
+        }
+
+        /**
+         * Returns whether fields in the nested document should be indexed according to that
+         * document's schema.
+         *
+         * <p>If false, the nested document's properties are not indexed regardless of its own
+         * schema.
+         */
+        public boolean shouldIndexNestedProperties() {
+            return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);
+        }
+
+        /** Builder for {@link DocumentPropertyConfig}. */
+        public static final class Builder {
+            private final String mPropertyName;
+            // TODO(b/181887768): This should be final
+            private String mSchemaType;
+            private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
+            private boolean mShouldIndexNestedProperties = false;
+
+            /**
+             * Creates a new {@link DocumentPropertyConfig.Builder}.
+             *
+             * @param propertyName The logical name of the property in the schema, which will be
+             *                     used as the key for this property in
+             *                     {@link GenericDocument.Builder#setPropertyDocument}.
+             * @param schemaType The type of documents which will be stored in this property.
+             *                   Documents of different types cannot be mixed into a single
+             *                   property.
+             */
+            public Builder(@NonNull String propertyName, @NonNull String schemaType) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+                mSchemaType = Preconditions.checkNotNull(schemaType);
+            }
+
+            /**
+             * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+             * @hide
+             */
+            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+            @Deprecated
+            /*@exportToFramework:UnsupportedAppUsage*/
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+                mSchemaType = null;
+            }
+
+            /**
+             * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+             * @hide
+             */
+            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+            @Deprecated
+            /*@exportToFramework:UnsupportedAppUsage*/
+            @NonNull
+            public Builder setSchemaType(@NonNull String schemaType) {
+                mSchemaType = Preconditions.checkNotNull(schemaType);
                 return this;
             }
 
             /**
              * The cardinality of the property (whether it is optional, required or repeated).
              *
-             * <p>This property must be set.
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
              */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
             @NonNull
-            public PropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
+            public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
                 Preconditions.checkArgumentInRange(
                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
-                mBundle.putInt(CARDINALITY_FIELD, cardinality);
+                mCardinality = cardinality;
                 return this;
             }
 
             /**
-             * Configures how a property should be indexed so that it can be retrieved by queries.
+             * Configures whether fields in the nested document should be indexed according to that
+             * document's schema.
+             *
+             * <p>If false, the nested document's properties are not indexed regardless of its own
+             * schema.
              */
             @NonNull
-            public PropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                Preconditions.checkArgumentInRange(
-                        indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
-                mBundle.putInt(INDEXING_TYPE_FIELD, indexingType);
-                return this;
-            }
-
-            /** Configures how this property should be tokenized (split into words). */
-            @NonNull
-            public PropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                Preconditions.checkArgumentInRange(
-                        tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
-                mBundle.putInt(TOKENIZER_TYPE_FIELD, tokenizerType);
+            public DocumentPropertyConfig.Builder setShouldIndexNestedProperties(
+                    boolean indexNestedProperties) {
+                mShouldIndexNestedProperties = indexNestedProperties;
                 return this;
             }
 
             /**
-             * Constructs a new {@link PropertyConfig} from the contents of this builder.
-             *
-             * <p>After calling this method, the builder must no longer be used.
-             *
-             * @throws IllegalSchemaException If the property is not correctly populated (e.g.
-             *     missing {@code dataType}).
+             * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+             * @hide
              */
+            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+            @Deprecated
+            /*@exportToFramework:UnsupportedAppUsage*/
             @NonNull
-            public PropertyConfig build() {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                // TODO(b/147692920): Send the schema to Icing Lib for official validation, instead
-                //     of partially reimplementing some of the validation Icing does here.
-                if (!mBundle.containsKey(DATA_TYPE_FIELD)) {
-                    throw new IllegalSchemaException("Missing field: dataType");
-                }
-                if (mBundle.getString(SCHEMA_TYPE_FIELD, "").isEmpty()
-                        && mBundle.getInt(DATA_TYPE_FIELD) == DATA_TYPE_DOCUMENT) {
-                    throw new IllegalSchemaException(
-                            "Missing field: schemaType (required for configs with "
-                                    + "dataType = DOCUMENT)");
-                }
-                if (!mBundle.containsKey(CARDINALITY_FIELD)) {
-                    throw new IllegalSchemaException("Missing field: cardinality");
-                }
-                mBuilt = true;
-                return new PropertyConfig(mBundle);
+            public DocumentPropertyConfig.Builder setIndexNestedProperties(
+                    boolean indexNestedProperties) {
+                return setShouldIndexNestedProperties(indexNestedProperties);
             }
+
+            /** Constructs a new {@link PropertyConfig} from the contents of this builder. */
+            @NonNull
+            public DocumentPropertyConfig build() {
+                Bundle bundle = new Bundle();
+                bundle.putString(NAME_FIELD, mPropertyName);
+                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);
+                bundle.putInt(CARDINALITY_FIELD, mCardinality);
+                bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties);
+                // TODO(b/181887768): Remove checkNotNull after the deprecated constructor (which
+                //  is the only way to get null here) is removed
+                bundle.putString(SCHEMA_TYPE_FIELD, Preconditions.checkNotNull(mSchemaType));
+                return new DocumentPropertyConfig(bundle);
+            }
+        }
+
+        /**
+         * Appends a debug string for the {@link DocumentPropertyConfig} instance to the given
+         * string builder.
+         *
+         * <p>This appends fields specific to a {@link DocumentPropertyConfig} instance.
+         *
+         * @param builder        the builder to append to.
+         */
+        void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
+            builder
+                    .append("shouldIndexNestedProperties: ")
+                    .append(shouldIndexNestedProperties())
+                    .append(",\n");
+
+            builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 76ce163..4c4c8c8 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -26,176 +26,201 @@
 import java.util.Set;
 
 /**
- * Represents a connection to an AppSearch storage system where {@link GenericDocument}s can be
- * placed and queried.
+ * Provides a connection to a single AppSearch database.
  *
- * All implementations of this interface must be thread safe.
+ * <p>An {@link AppSearchSession} instance provides access to database operations such as setting
+ * a schema, adding documents, and searching.
+ *
+ * <p>Instances of this interface are usually obtained from a storage implementation, e.g.
+ * {@code LocalStorage.createSearchSession()} or {@code PlatformStorage.createSearchSession()}.
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see GlobalSearchSession
  */
 public interface AppSearchSession extends Closeable {
 
     /**
-     * Sets the schema that will be used by documents provided to the {@link #putDocuments} method.
+     * Sets the schema that represents the organizational structure of data within the AppSearch
+     * database.
      *
-     * <p>The schema provided here is compared to the stored copy of the schema previously supplied
-     * to {@link #setSchema}, if any, to determine how to treat existing documents. The following
-     * types of schema modifications are always safe and are made without deleting any existing
-     * documents:
-     * <ul>
-     *     <li>Addition of new types
-     *     <li>Addition of new
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL OPTIONAL} or
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED REPEATED} properties to a
-     *         type
-     *     <li>Changing the cardinality of a data type to be less restrictive (e.g. changing an
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL OPTIONAL} property into a
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED REPEATED} property.
-     * </ul>
+     * <p>Upon creating an {@link AppSearchSession}, {@link #setSchema} should be called. If the
+     * schema needs to be updated, or it has not been previously set, then the provided schema
+     * will be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently
+     * as a no-op call.
      *
-     * <p>The following types of schema changes are not backwards-compatible:
-     * <ul>
-     *     <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 Document} type, changing the schema type of
-     *         {@code Document}s of that property
-     *     <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL OPTIONAL} property into a
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED REQUIRED} property).
-     *     <li>Adding a
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED REQUIRED} property.
-     * </ul>
-     * <p>Supplying a schema with such changes will, by default, result in this call completing its
-     * future with an {@link androidx.appsearch.exceptions.AppSearchException} with a code of
-     * {@link AppSearchResult#RESULT_INVALID_SCHEMA} and a message describing the incompatibility.
-     * In this case the previously set schema will remain active.
-     *
-     * <p>If you need to make non-backwards-compatible changes as described above, you can set the
-     * {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}. In this case,
-     * instead of completing its future with an
-     * {@link androidx.appsearch.exceptions.AppSearchException} with the
-     * {@link AppSearchResult#RESULT_INVALID_SCHEMA} error code, all documents which are not
-     * compatible with the new schema will be deleted and the incompatible schema will be applied.
-     *
-     * <p>It is a no-op to set the same schema as has been previously set; this is handled
-     * efficiently.
-     *
-     * <p>By default, documents are visible on platform surfaces. To opt out, call {@code
-     * SetSchemaRequest.Builder#setPlatformSurfaceable} with {@code surfaceable} as false. Any
-     * visibility settings apply only to the schemas that are included in the {@code request}.
-     * Visibility settings for a schema type do not apply or persist across
-     * {@link SetSchemaRequest}s.
-     *
-     * @param request The schema update request.
-     * @return The pending result of performing this operation.
+     * @param  request the schema to set or update the AppSearch database to.
+     * @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
      */
-    // TODO(b/169883602): Change @code references to @link when setPlatformSurfaceable APIs are
-    //  exposed.
     @NonNull
-    ListenableFuture<Void> setSchema(@NonNull SetSchemaRequest request);
+    ListenableFuture<SetSchemaResponse> setSchema(
+            @NonNull SetSchemaRequest request);
 
     /**
      * Retrieves the schema most recently successfully provided to {@link #setSchema}.
      *
-     * @return The pending result of performing this operation.
+     * @return The pending {@link GetSchemaResponse} of performing this operation.
      */
     // This call hits disk; async API prevents us from treating these calls as properties.
     @SuppressLint("KotlinPropertyAccess")
     @NonNull
-    ListenableFuture<Set<AppSearchSchema>> getSchema();
+    ListenableFuture<GetSchemaResponse> getSchema();
 
     /**
-     * Indexes documents into AppSearch.
+     * Retrieves the set of all namespaces in the current database with at least one document.
      *
-     * <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 request {@link PutDocumentsRequest} containing documents to be indexed
-     * @return The pending result of performing this operation. The keys of the returned
-     * {@link AppSearchBatchResult} are the URIs of the input documents. The values are
-     * {@code null} if they were successfully indexed, or a failed {@link AppSearchResult}
-     * otherwise.
+     * @return The pending result of performing this operation.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, Void>> putDocuments(
-            @NonNull PutDocumentsRequest request);
+    ListenableFuture<Set<String>> getNamespaces();
 
     /**
-     * Retrieves {@link GenericDocument}s by URI.
+     * Indexes documents into the {@link AppSearchSession} database.
      *
-     * @param request {@link GetByUriRequest} containing URIs to be retrieved.
-     * @return The pending result of performing this operation. The keys of the returned
-     * {@link AppSearchBatchResult} are the input URIs. The values are the returned
-     * {@link GenericDocument}s on success, or a failed {@link AppSearchResult} otherwise.
-     * URIs that are not found will return a failed {@link AppSearchResult} with a result code
-     * of {@link AppSearchResult#RESULT_NOT_FOUND}.
+     * <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an
+     * {@link AppSearchSchema} type that has been previously registered by calling the
+     * {@link #setSchema} method.
+     *
+     * @param request containing documents to be indexed.
+     * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+     * The keys of the returned {@link AppSearchBatchResult} are the IDs of the input documents.
+     * The values are either {@code null} if the corresponding document was successfully indexed,
+     * or a failed {@link AppSearchResult} otherwise.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByUri(
-            @NonNull GetByUriRequest request);
+    ListenableFuture<AppSearchBatchResult<String, Void>> put(@NonNull PutDocumentsRequest request);
 
     /**
-     * Searches a document based on a given query string.
+     * Gets {@link GenericDocument} objects by document IDs in a namespace from the
+     * {@link AppSearchSession} database.
      *
-     * <p>Currently we support following features in the raw query format:
-     * <ul>
-     *     <li>AND
-     *     <p>AND joins (e.g. “match documents that have both the terms ‘dog’ and
-     *     ‘cat’”).
-     *     Example: hello world matches documents that have both ‘hello’ and ‘world’
-     *     <li>OR
-     *     <p>OR joins (e.g. “match documents that have either the term ‘dog’ or
-     *     ‘cat’”).
-     *     Example: dog OR puppy
-     *     <li>Exclusion
-     *     <p>Exclude a term (e.g. “match documents that do
-     *     not have the term ‘dog’”).
-     *     Example: -dog excludes the term ‘dog’
-     *     <li>Grouping terms
-     *     <p>Allow for conceptual grouping of subqueries to enable hierarchical structures (e.g.
-     *     “match documents that have either ‘dog’ or ‘puppy’, and either ‘cat’ or ‘kitten’”).
-     *     Example: (dog puppy) (cat kitten) two one group containing two terms.
-     *     <li>Property restricts
-     *     <p> Specifies which properties of a document to specifically match terms in (e.g.
-     *     “match documents where the ‘subject’ property contains ‘important’”).
-     *     Example: subject:important matches documents with the term ‘important’ in the
-     *     ‘subject’ property
-     *     <li>Schema type restricts
-     *     <p>This is similar to property restricts, but allows for restricts on top-level document
-     *     fields, such as schema_type. Clients should be able to limit their query to documents of
-     *     a certain schema_type (e.g. “match documents that are of the ‘Email’ schema_type”).
-     *     Example: { schema_type_filters: “Email”, “Video”,query: “dog” } will match documents
-     *     that contain the query term ‘dog’ and are of either the ‘Email’ schema type or the
-     *     ‘Video’ schema type.
-     * </ul>
-     *
-     * <p> This method is lightweight. The heavy work will be done in
-     * {@link SearchResults#getNextPage()}.
-     *
-     * @param queryExpression Query String to search.
-     * @param searchSpec      Spec for setting filters, raw query etc.
-     * @return The search result of performing this operation.
-     */
-    @NonNull
-    SearchResults query(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
-
-    /**
-     * Removes {@link GenericDocument}s from the index by URI.
-     *
-     * @param request Request containing URIs to be removed.
-     * @return The pending result of performing this operation. The keys of the returned
-     * {@link AppSearchBatchResult} are the input URIs. The values are {@code null} on success,
-     * or a failed {@link AppSearchResult} otherwise. URIs that are not found will return a
-     * failed {@link AppSearchResult} with a result code of
+     * @param request a request containing a namespace and IDs to get documents for.
+     * @return A {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+     * The keys of the {@link AppSearchBatchResult} represent the input document IDs from the
+     * {@link GetByDocumentIdRequest} object. The values are either the corresponding
+     * {@link GenericDocument} object for the ID on success, or an {@link AppSearchResult}
+     * object on failure. For example, if an ID is not found, the value for that ID will be set
+     * to an {@link AppSearchResult} object with result code:
      * {@link AppSearchResult#RESULT_NOT_FOUND}.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, Void>> removeByUri(
-            @NonNull RemoveByUriRequest request);
+    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest request);
+
+    /**
+     * Retrieves documents from the open {@link AppSearchSession} that match a given query string
+     * and type of search provided.
+     *
+     * <p>Query strings can be empty, contain one term with no operators, or contain multiple
+     * terms and operators.
+     *
+     * <p>For query strings that are empty, all documents that match the {@link SearchSpec} will be
+     * returned.
+     *
+     * <p>For query strings with a single term and no operators, documents that match the
+     * provided query string and {@link SearchSpec} will be returned.
+     *
+     * <p>The following operators are supported:
+     *
+     * <ul>
+     *     <li>AND (implicit)
+     *     <p>AND is an operator that matches documents that contain <i>all</i>
+     *     provided terms.
+     *     <p><b>NOTE:</b> A space between terms is treated as an "AND" operator. Explicitly
+     *     including "AND" in a query string will treat "AND" as a term, returning documents that
+     *     also contain "AND".
+     *     <p>Example: "apple AND banana" matches documents that contain the
+     *     terms "apple", "and", "banana".
+     *     <p>Example: "apple banana" matches documents that contain both "apple" and
+     *     "banana".
+     *     <p>Example: "apple banana cherry" matches documents that contain "apple", "banana", and
+     *     "cherry".
+     *
+     *     <li>OR
+     *     <p>OR is an operator that matches documents that contain <i>any</i> provided term.
+     *     <p>Example: "apple OR banana" matches documents that contain either "apple" or "banana".
+     *     <p>Example: "apple OR banana OR cherry" matches documents that contain any of
+     *     "apple", "banana", or "cherry".
+     *
+     *     <li>Exclusion (-)
+     *     <p>Exclusion (-) is an operator that matches documents that <i>do not</i> contain the
+     *     provided term.
+     *     <p>Example: "-apple" matches documents that do not contain "apple".
+     *
+     *     <li>Grouped Terms
+     *     <p>For queries that require multiple operators and terms, terms can be grouped into
+     *     subqueries. Subqueries are contained within an open "(" and close ")" parenthesis.
+     *     <p>Example: "(donut OR bagel) (coffee OR tea)" matches documents that contain
+     *     either "donut" or "bagel" and either "coffee" or "tea".
+     *
+     *     <li>Property Restricts
+     *     <p>For queries that require a term to match a specific {@link AppSearchSchema}
+     *     property of a document, a ":" must be included between the property name and the term.
+     *     <p>Example: "subject:important" matches documents that contain the term "important" in
+     *     the "subject" property.
+     * </ul>
+     *
+     * <p>Additional search specifications, such as filtering by {@link AppSearchSchema} type or
+     * adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
+     *
+     * <p>This method is lightweight. The heavy work will be done in
+     * {@link SearchResults#getNextPage}.
+     *
+     * @param queryExpression query string to search.
+     * @param searchSpec      spec for setting document filters, adding projection, setting term
+     *                        match type, etc.
+     * @return a {@link SearchResults} object for retrieved matched documents.
+     */
+    @NonNull
+    SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Reports usage of a particular document by namespace and ID.
+     *
+     * <p>A usage report represents an event in which a user interacted with or viewed a document.
+     *
+     * <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency
+     * metrics for that particular document. These metrics are used for ordering {@link #search}
+     * results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and
+     * {@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
+     *
+     * <p>Reporting usage of a document is optional.
+     *
+     * @param request The usage reporting request.
+     * @return The pending result of performing this operation which resolves to {@code null} on
+     *     success.
+     */
+    @NonNull
+    ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request);
+
+    /**
+     * Removes {@link GenericDocument} objects by document IDs in a namespace from the
+     * {@link AppSearchSession} database.
+     *
+     * <p>Removed documents will no longer be surfaced by {@link #search} or
+     * {@link #getByDocumentId}
+     * calls.
+     *
+     * <p>Once the database crosses the document count or byte usage threshold, removed documents
+     * will be deleted from disk.
+     *
+     * @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
+     *                index.
+     * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+     * The keys of the {@link AppSearchBatchResult} represent the input IDs from the
+     * {@link RemoveByDocumentIdRequest} object. The values are either {@code null} on success,
+     * or a failed {@link AppSearchResult} otherwise. IDs that are not found will return a failed
+     * {@link AppSearchResult} with a result code of {@link AppSearchResult#RESULT_NOT_FOUND}.
+     */
+    @NonNull
+    ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest request);
 
     /**
      * Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
      * match the {@code queryExpression} in given namespaces and schemaTypes which is set via
-     * {@link SearchSpec.Builder#addNamespace} and {@link SearchSpec.Builder#addSchemaType}.
+     * {@link SearchSpec.Builder#addFilterNamespaces} and
+     * {@link SearchSpec.Builder#addFilterSchemas}.
      *
      * <p> An empty {@code queryExpression} matches all documents.
      *
@@ -209,8 +234,32 @@
      * @return The pending result of performing this operation.
      */
     @NonNull
-    ListenableFuture<Void> removeByQuery(
-            @NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+    ListenableFuture<Void> remove(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Gets the storage info for this {@link AppSearchSession} database.
+     *
+     * <p>This may take time proportional to the number of documents and may be inefficient to
+     * call repeatedly.
+     *
+     * @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
+     */
+    @NonNull
+    ListenableFuture<StorageInfo> getStorageInfo();
+
+    /**
+     * Flush all schema and document updates, additions, and deletes to disk if possible.
+     *
+     * <p>The request is not guaranteed to be handled and may be ignored by some implementations of
+     * AppSearchSession.
+     *
+     * @return The pending result of performing this operation.
+     * {@link androidx.appsearch.exceptions.AppSearchException} with
+     * {@link AppSearchResult#RESULT_INTERNAL_ERROR} will be set to the future if we hit error when
+     * save to disk.
+     */
+    @NonNull
+    ListenableFuture<Void> requestFlush();
 
     /**
      * Closes the {@link AppSearchSession} to persist all schema and document updates, additions,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactoryRegistry.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactoryRegistry.java
deleted file mode 100644
index 3be56c6..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactoryRegistry.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.
- */
-// @exportToFramework:skipFile()
-package androidx.appsearch.app;
-
-import androidx.annotation.AnyThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.appsearch.exceptions.AppSearchException;
-import androidx.core.util.Preconditions;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A registry which maintains instances of {@link androidx.appsearch.app.DataClassFactory}.
- * @hide
- */
-@AnyThread
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public final class DataClassFactoryRegistry {
-    private static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
-
-    private static volatile DataClassFactoryRegistry sInstance = null;
-
-    private final Map<Class<?>, DataClassFactory<?>> mFactories = new HashMap<>();
-
-    private DataClassFactoryRegistry() {}
-
-    /** Returns the singleton instance of {@link DataClassFactoryRegistry}. */
-    @NonNull
-    public static DataClassFactoryRegistry getInstance() {
-        if (sInstance == null) {
-            synchronized (DataClassFactoryRegistry.class) {
-                if (sInstance == null) {
-                    sInstance = new DataClassFactoryRegistry();
-                }
-            }
-        }
-        return sInstance;
-    }
-
-    /**
-     * Gets the {@link DataClassFactory} instance that can convert to and from objects of type
-     * {@code T}.
-     *
-     * @throws AppSearchException if no factory for this data class could be found on the classpath
-     */
-    @NonNull
-    @SuppressWarnings("unchecked")
-    public <T> DataClassFactory<T> getOrCreateFactory(@NonNull Class<T> dataClass)
-            throws AppSearchException {
-        Preconditions.checkNotNull(dataClass);
-        DataClassFactory<?> factory;
-        synchronized (this) {
-            factory = mFactories.get(dataClass);
-        }
-        if (factory == null) {
-            factory = loadFactoryByReflection(dataClass);
-            synchronized (this) {
-                DataClassFactory<?> racingFactory = mFactories.get(dataClass);
-                if (racingFactory == null) {
-                    mFactories.put(dataClass, factory);
-                } else {
-                    // Another thread beat us to it
-                    factory = racingFactory;
-                }
-            }
-        }
-        return (DataClassFactory<T>) factory;
-    }
-
-    /**
-     * Gets the {@link DataClassFactory} instance that can convert to and from objects of type
-     * {@code T}.
-     *
-     * @throws AppSearchException if no factory for this data class could be found on the classpath
-     */
-    @NonNull
-    @SuppressWarnings("unchecked")
-    public <T> DataClassFactory<T> getOrCreateFactory(@NonNull T dataClass)
-            throws AppSearchException {
-        Preconditions.checkNotNull(dataClass);
-        Class<?> clazz = dataClass.getClass();
-        DataClassFactory<?> factory = getOrCreateFactory(clazz);
-        return (DataClassFactory<T>) factory;
-    }
-
-    private DataClassFactory<?> loadFactoryByReflection(@NonNull Class<?> dataClass)
-            throws AppSearchException {
-        Package pkg = dataClass.getPackage();
-        String simpleName = dataClass.getCanonicalName();
-        if (simpleName == null) {
-            throw new AppSearchException(
-                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                    "Failed to find simple name for data class \"" + dataClass
-                            + "\". Perhaps it is anonymous?");
-        }
-
-        // Creates factory class name under the package.
-        // For a class Foo annotated with @AppSearchDocument, we will generated a
-        // $$__AppSearch__Foo.class under the package.
-        // For an inner class Foo.Bar annotated with @AppSearchDocument, we will generated a
-        // $$__AppSearch__Foo$$__Bar.class under the package.
-        String packageName = "";
-        if (pkg != null) {
-            packageName = pkg.getName() + ".";
-            simpleName = simpleName.substring(packageName.length()).replace(".", "$$__");
-        }
-        String factoryClassName = packageName + GEN_CLASS_PREFIX + simpleName;
-
-        Class<?> factoryClass;
-        try {
-            factoryClass = Class.forName(factoryClassName);
-        } catch (ClassNotFoundException e) {
-            throw new AppSearchException(
-                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                    "Failed to find data class converter \"" + factoryClassName
-                            + "\". Perhaps the annotation processor was not run or the class was "
-                            + "proguarded out?",
-                    e);
-        }
-        Object instance;
-        try {
-            instance = factoryClass.getDeclaredConstructor().newInstance();
-        } catch (Exception e) {
-            throw new AppSearchException(
-                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                    "Failed to construct data class converter \"" + factoryClassName + "\"",
-                    e);
-        }
-        return (DataClassFactory<?>) instance;
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactory.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
similarity index 63%
rename from appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactory.java
rename to appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
index 014b23b..bd03221 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactory.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
@@ -20,31 +20,35 @@
 import androidx.appsearch.exceptions.AppSearchException;
 
 /**
- * An interface for factories which can convert between data classes and {@link GenericDocument}.
+ * An interface for factories which can convert between instances of classes annotated with
+ * \@{@link androidx.appsearch.annotation.Document} and instances of {@link GenericDocument}.
  *
- * @param <T> The type of data class this factory converts to and from {@link GenericDocument}.
+ * @param <T> The document class type this factory converts to and from {@link GenericDocument}.
  */
-public interface DataClassFactory<T> {
+public interface DocumentClassFactory<T> {
     /**
      * Returns the name of this schema type, e.g. {@code Email}.
      *
      * <p>This is the name used in queries for type restricts.
      */
     @NonNull
-    String getSchemaType();
+    String getSchemaName();
 
-    /** Returns the schema for this data class. */
+    /** Returns the schema for this document class. */
     @NonNull
     AppSearchSchema getSchema() throws AppSearchException;
 
     /**
-     * Converts an instance of the data class into a {@link androidx.appsearch.app.GenericDocument}.
+     * Converts an instance of the class annotated with
+     * \@{@link androidx.appsearch.annotation.Document} into a
+     * {@link androidx.appsearch.app.GenericDocument}.
      */
     @NonNull
-    GenericDocument toGenericDocument(@NonNull T dataClass) throws AppSearchException;
+    GenericDocument toGenericDocument(@NonNull T document) throws AppSearchException;
 
     /**
-     * Converts a {@link androidx.appsearch.app.GenericDocument} into an instance of the data class.
+     * Converts a {@link androidx.appsearch.app.GenericDocument} into an instance of the document
+     * class.
      */
     @NonNull
     T fromGenericDocument(@NonNull GenericDocument genericDoc) throws AppSearchException;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactoryRegistry.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactoryRegistry.java
new file mode 100644
index 0000000..6ce9a9b6
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactoryRegistry.java
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.core.util.Preconditions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A registry which maintains instances of {@link DocumentClassFactory}.
+ * @hide
+ */
+@AnyThread
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class DocumentClassFactoryRegistry {
+    private static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
+
+    private static volatile DocumentClassFactoryRegistry sInstance = null;
+
+    private final Map<Class<?>, DocumentClassFactory<?>> mFactories = new HashMap<>();
+
+    private DocumentClassFactoryRegistry() {}
+
+    /** Returns the singleton instance of {@link DocumentClassFactoryRegistry}. */
+    @NonNull
+    public static DocumentClassFactoryRegistry getInstance() {
+        if (sInstance == null) {
+            synchronized (DocumentClassFactoryRegistry.class) {
+                if (sInstance == null) {
+                    sInstance = new DocumentClassFactoryRegistry();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
+     * {@code T}.
+     *
+     * @throws AppSearchException if no factory for this document class could be found on the
+     * classpath
+     */
+    @NonNull
+    @SuppressWarnings("unchecked")
+    public <T> DocumentClassFactory<T> getOrCreateFactory(@NonNull Class<T> documentClass)
+            throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        DocumentClassFactory<?> factory;
+        synchronized (this) {
+            factory = mFactories.get(documentClass);
+        }
+        if (factory == null) {
+            factory = loadFactoryByReflection(documentClass);
+            synchronized (this) {
+                DocumentClassFactory<?> racingFactory = mFactories.get(documentClass);
+                if (racingFactory == null) {
+                    mFactories.put(documentClass, factory);
+                } else {
+                    // Another thread beat us to it
+                    factory = racingFactory;
+                }
+            }
+        }
+        return (DocumentClassFactory<T>) factory;
+    }
+
+    /**
+     * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
+     * {@code T}.
+     *
+     * @throws AppSearchException if no factory for this document class could be found on the
+     * classpath
+     */
+    @NonNull
+    @SuppressWarnings("unchecked")
+    public <T> DocumentClassFactory<T> getOrCreateFactory(@NonNull T documentClass)
+            throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        Class<?> clazz = documentClass.getClass();
+        DocumentClassFactory<?> factory = getOrCreateFactory(clazz);
+        return (DocumentClassFactory<T>) factory;
+    }
+
+    private DocumentClassFactory<?> loadFactoryByReflection(@NonNull Class<?> documentClass)
+            throws AppSearchException {
+        Package pkg = documentClass.getPackage();
+        String simpleName = documentClass.getCanonicalName();
+        if (simpleName == null) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR,
+                    "Failed to find simple name for document class \"" + documentClass
+                            + "\". Perhaps it is anonymous?");
+        }
+
+        // Creates factory class name under the package.
+        // For a class Foo annotated with @Document, we will generated a
+        // $$__AppSearch__Foo.class under the package.
+        // For an inner class Foo.Bar annotated with @Document, we will generated a
+        // $$__AppSearch__Foo$$__Bar.class under the package.
+        String packageName = "";
+        if (pkg != null) {
+            packageName = pkg.getName() + ".";
+            simpleName = simpleName.substring(packageName.length()).replace(".", "$$__");
+        }
+        String factoryClassName = packageName + GEN_CLASS_PREFIX + simpleName;
+
+        Class<?> factoryClass;
+        try {
+            factoryClass = Class.forName(factoryClassName);
+        } catch (ClassNotFoundException e) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR,
+                    "Failed to find document class converter \"" + factoryClassName
+                            + "\". Perhaps the annotation processor was not run or the class was "
+                            + "proguarded out?",
+                    e);
+        }
+        Object instance;
+        try {
+            instance = factoryClass.getDeclaredConstructor().newInstance();
+        } catch (Exception e) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR,
+                    "Failed to construct document class converter \"" + factoryClassName + "\"",
+                    e);
+        }
+        return (DocumentClassFactory<?>) instance;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 77d3918..c1e5439 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -18,49 +18,46 @@
 
 import android.annotation.SuppressLint;
 import android.os.Bundle;
+import android.os.Parcelable;
 import android.util.Log;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.Document;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.util.IndentingStringBuilder;
 import androidx.core.util.Preconditions;
 
 import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 /**
  * Represents a document unit.
  *
- * <p>Documents are constructed via {@link GenericDocument.Builder}.
+ * <p>Documents contain structured data conforming to their {@link AppSearchSchema} type.
+ * Each document is uniquely identified by a namespace and a String ID within that namespace.
  *
- * @see AppSearchSession#putDocuments
- * @see AppSearchSession#getByUri
- * @see AppSearchSession#query
+ * <!--@exportToFramework:ifJetpack()-->
+ * <p>Documents are constructed either by using the {@link GenericDocument.Builder} or providing
+ * an annotated {@link Document} data class.
+ * <!--@exportToFramework:else()
+ * <p>Documents are constructed by using the {@link GenericDocument.Builder}.
+ * -->
+ *
+ * @see AppSearchSession#put
+ * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#search
  */
 public class GenericDocument {
     private static final String TAG = "AppSearchGenericDocumen";
 
-    /** 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;
 
@@ -73,7 +70,7 @@
     private static final String PROPERTIES_FIELD = "properties";
     private static final String BYTE_ARRAY_FIELD = "byteArray";
     private static final String SCHEMA_TYPE_FIELD = "schemaType";
-    private static final String URI_FIELD = "uri";
+    private static final String ID_FIELD = "id";
     private static final String SCORE_FIELD = "score";
     private static final String TTL_MILLIS_FIELD = "ttlMillis";
     private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
@@ -82,24 +79,51 @@
     /**
      * The maximum number of indexed properties a document can have.
      *
-     * <p>Indexed properties are properties where the
-     * {@link AppSearchSchema.PropertyConfig#getIndexingType()} constant is anything other than
-     * {@link AppSearchSchema.PropertyConfig.IndexingType#INDEXING_TYPE_NONE}.
+     * <p>Indexed properties are properties which are strings where the
+     * {@link AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other
+     * than {@link AppSearchSchema.StringPropertyConfig.IndexingType#INDEXING_TYPE_NONE}.
      */
     public static int getMaxIndexedProperties() {
         return MAX_INDEXED_PROPERTIES;
     }
 
-    /** Contains {@link GenericDocument} basic information (uri, schemaType etc). */
+// @exportToFramework:startStrip()
+
+    /**
+     * Converts an instance of a class annotated with \@{@link Document} into an instance of
+     * {@link GenericDocument}.
+     *
+     * @param document An instance of a class annotated with \@{@link Document}.
+     * @return an instance of {@link GenericDocument} produced by converting {@code document}.
+     * @throws AppSearchException if no generated conversion class exists on the classpath for the
+     *                            given document class or an unexpected error occurs during
+     *                            conversion.
+     * @see GenericDocument#toDocumentClass
+     */
+    @NonNull
+    public static GenericDocument fromDocumentClass(@NonNull Object document)
+            throws AppSearchException {
+        Preconditions.checkNotNull(document);
+        DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+        DocumentClassFactory<Object> factory = registry.getOrCreateFactory(document);
+        return factory.toGenericDocument(document);
+    }
+// @exportToFramework:endStrip()
+
+    /**
+     * Contains all {@link GenericDocument} information in a packaged format.
+     *
+     * <p>Keys are the {@code *_FIELD} constants in this class.
+     */
     @NonNull
     final Bundle mBundle;
 
-    /** Contains all properties in {@link GenericDocument} to support getting properties via keys */
+    /** Contains all properties in {@link GenericDocument} to support getting properties via name */
     @NonNull
     private final Bundle mProperties;
 
     @NonNull
-    private final String mUri;
+    private final String mId;
     @NonNull
     private final String mSchemaType;
     private final long mCreationTimestampMillis;
@@ -107,11 +131,10 @@
     private Integer mHashCode;
 
     /**
-     * Rebuilds a {@link GenericDocument} by the a bundle.
+     * Rebuilds a {@link GenericDocument} from 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.
+     * @param bundle Packaged {@link GenericDocument} data, such as the result of
+     *               {@link #getBundle}.
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -119,7 +142,7 @@
         Preconditions.checkNotNull(bundle);
         mBundle = bundle;
         mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD));
-        mUri = Preconditions.checkNotNull(mBundle.getString(URI_FIELD));
+        mId = Preconditions.checkNotNull(mBundle.getString(ID_FIELD));
         mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
         mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD,
                 System.currentTimeMillis());
@@ -145,19 +168,31 @@
         return mBundle;
     }
 
-    /** Returns the URI of the {@link GenericDocument}. */
+    /**
+     * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Deprecated
+    /*@exportToFramework:UnsupportedAppUsage*/
     @NonNull
     public String getUri() {
-        return mUri;
+        return getId();
+    }
+
+    /** Returns the unique identifier of the {@link GenericDocument}. */
+    @NonNull
+    public String getId() {
+        return mId;
     }
 
     /** Returns the namespace of the {@link GenericDocument}. */
     @NonNull
     public String getNamespace() {
-        return mBundle.getString(NAMESPACE_FIELD, DEFAULT_NAMESPACE);
+        return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ "");
     }
 
-    /** Returns the schema type of the {@link GenericDocument}. */
+    /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
     @NonNull
     public String getSchemaType() {
         return mSchemaType;
@@ -168,19 +203,20 @@
      *
      * <p>The value is in the {@link System#currentTimeMillis} time base.
      */
+    /*@exportToFramework:CurrentTimeMillisLong*/
     public long getCreationTimestampMillis() {
         return mCreationTimestampMillis;
     }
 
     /**
-     * Returns the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
+     * Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
      *
      * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
      * {@code creationTimestampMillis + ttlMillis}, measured in the {@link System#currentTimeMillis}
      * time base, the document will be auto-deleted.
      *
      * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
-     * until the app is uninstalled.
+     * until the app is uninstalled or {@link AppSearchSession#remove} is called.
      */
     public long getTtlMillis() {
         return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
@@ -190,12 +226,12 @@
      * 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.
+     * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
      *
      * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
      * Documents with higher scores are considered better than documents with lower scores.
      *
-     * <p>Any nonnegative integer can be used a score.
+     * <p>Any non-negative integer can be used a score.
      */
     public int getScore() {
         return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);
@@ -208,133 +244,506 @@
     }
 
     /**
-     * Retrieves the property value with the given key as {@link Object}.
+     * Retrieves the property value with the given path as {@link Object}.
      *
-     * @param key The key to look for.
-     * @return The entry with the given key as an object or {@code null} if there is no such key.
+     * <p>A path can be a simple property name, such as those returned by {@link #getPropertyNames}.
+     * It may also be a dot-delimited path through the nested document hierarchy, with nested
+     * {@link GenericDocument} properties accessed via {@code '.'} and repeated properties
+     * optionally indexed into via {@code [n]}.
+     *
+     * <p>For example, given the following {@link GenericDocument}:
+     * <pre>
+     *     (Message) {
+     *         from: "sender@example.com"
+     *         to: [{
+     *             name: "Albert Einstein"
+     *             email: "einstein@example.com"
+     *           }, {
+     *             name: "Marie Curie"
+     *             email: "curie@example.com"
+     *           }]
+     *         tags: ["important", "inbox"]
+     *         subject: "Hello"
+     *     }
+     * </pre>
+     *
+     * <p>Here are some example paths and their results:
+     * <ul>
+     *     <li>{@code "from"} returns {@code "sender@example.com"} as a {@link String} array with
+     *     one element
+     *     <li>{@code "to"} returns the two nested documents containing contact information as a
+     *     {@link GenericDocument} array with two elements
+     *     <li>{@code "to[1]"} returns the second nested document containing Marie Curie's
+     *     contact information as a {@link GenericDocument} array with one element
+     *     <li>{@code "to[1].email"} returns {@code "curie@example.com"}
+     *     <li>{@code "to[100].email"} returns {@code null} as this particular document does not
+     *     have that many elements in its {@code "to"} array.
+     *     <li>{@code "to.email"} aggregates emails across all nested documents that have them,
+     *     returning {@code ["einstein@example.com", "curie@example.com"]} as a {@link String}
+     *     array with two elements.
+     * </ul>
+     *
+     * <p>If you know the expected type of the property you are retrieving, it is recommended to use
+     * one of the typed versions of this method instead, such as {@link #getPropertyString} or
+     * {@link #getPropertyStringArray}.
+     *
+     * @param path The path to look for.
+     * @return The entry with the given path as an object or {@code null} if there is no such path.
+     *   The returned object will be one of the following types: {@code String[]}, {@code long[]},
+     *   {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
      */
     @Nullable
-    public Object getProperty(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        Object property = mProperties.get(key);
-        if (property instanceof ArrayList) {
-            return getPropertyBytesArray(key);
-        } else if (property instanceof Bundle[]) {
-            return getPropertyDocumentArray(key);
+    public Object getProperty(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object rawValue = getRawPropertyFromRawDocument(path, mBundle);
+
+        // Unpack the raw value into the types the user expects, if required.
+        if (rawValue instanceof Bundle) {
+            // getRawPropertyFromRawDocument may return a document as a bare Bundle as a performance
+            // optimization for lookups.
+            GenericDocument document = new GenericDocument((Bundle) rawValue);
+            return new GenericDocument[]{document};
         }
-        return property;
+
+        if (rawValue instanceof List) {
+            // byte[][] fields are packed into List<Bundle> where each Bundle contains just a single
+            // entry: BYTE_ARRAY_FIELD -> byte[].
+            @SuppressWarnings("unchecked")
+            List<Bundle> bundles = (List<Bundle>) rawValue;
+            if (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 path: " + path);
+                    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;
+        }
+
+        if (rawValue instanceof Parcelable[]) {
+            // The underlying Bundle of nested GenericDocuments is packed into a Parcelable array.
+            // We must unpack it into GenericDocument instances.
+            Parcelable[] bundles = (Parcelable[]) rawValue;
+            if (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 path: " + path);
+                    continue;
+                }
+                if (!(bundles[i] instanceof Bundle)) {
+                    Log.e(TAG, "The inner element at " + i + " is a " + bundles[i].getClass()
+                            + ", not a Bundle for path: " + path);
+                    continue;
+                }
+                documents[i] = new GenericDocument((Bundle) bundles[i]);
+            }
+            return documents;
+        }
+
+        // Otherwise the raw property is the same as the final property and needs no transformation.
+        return rawValue;
     }
 
     /**
-     * Retrieves a {@link String} value by key.
+     * Looks up a property path within the given document bundle.
      *
-     * @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.
+     * <p>The return value may be any of GenericDocument's internal repeated storage types
+     * (String[], long[], double[], boolean[], ArrayList&lt;Bundle&gt;, Parcelable[]).
      */
     @Nullable
-    public String getPropertyString(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        String[] propertyArray = getPropertyStringArray(key);
+    private static Object getRawPropertyFromRawDocument(
+            @NonNull String path, @NonNull Bundle documentBundle) {
+        Preconditions.checkNotNull(path);
+        Preconditions.checkNotNull(documentBundle);
+        Bundle properties = Preconditions.checkNotNull(documentBundle.getBundle(PROPERTIES_FIELD));
+
+        // Determine whether the path is just a raw property name with no control characters
+        int controlIdx = -1;
+        boolean controlIsIndex = false;
+        for (int i = 0; i < path.length(); i++) {
+            char c = path.charAt(i);
+            if (c == '[' || c == '.') {
+                controlIdx = i;
+                controlIsIndex = c == '[';
+                break;
+            }
+        }
+
+        // Look up the value of the first path element
+        Object firstElementValue;
+        if (controlIdx == -1) {
+            firstElementValue = properties.get(path);
+        } else {
+            String name = path.substring(0, controlIdx);
+            firstElementValue = properties.get(name);
+        }
+
+        // If the path has no further elements, we're done.
+        if (firstElementValue == null || controlIdx == -1) {
+            return firstElementValue;
+        }
+
+        // At this point, for a path like "recipients[0]", firstElementValue contains the value of
+        // "recipients". If the first element of the path is an indexed value, we now update
+        // firstElementValue to contain "recipients[0]" instead.
+        String remainingPath;
+        if (!controlIsIndex) {
+            // Remaining path is everything after the .
+            remainingPath = path.substring(controlIdx + 1);
+        } else {
+            int endBracketIdx = path.indexOf(']', controlIdx);
+            if (endBracketIdx == -1) {
+                throw new IllegalArgumentException("Malformed path (no ending ']'): " + path);
+            }
+            if (endBracketIdx + 1 < path.length() && path.charAt(endBracketIdx + 1) != '.') {
+                throw new IllegalArgumentException(
+                        "Malformed path (']' not followed by '.'): " + path);
+            }
+            String indexStr = path.substring(controlIdx + 1, endBracketIdx);
+            int index = Integer.parseInt(indexStr);
+            if (index < 0) {
+                throw new IllegalArgumentException("Path index less than 0: " + path);
+            }
+
+            // Remaining path is everything after the [n]
+            if (endBracketIdx + 1 < path.length()) {
+                // More path remains, and we've already checked that charAt(endBracketIdx+1) == .
+                remainingPath = path.substring(endBracketIdx + 2);
+            } else {
+                // No more path remains.
+                remainingPath = null;
+            }
+
+            // Extract the right array element
+            Object extractedValue = null;
+            if (firstElementValue instanceof String[]) {
+                String[] stringValues = (String[]) firstElementValue;
+                if (index < stringValues.length) {
+                    extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof long[]) {
+                long[] longValues = (long[]) firstElementValue;
+                if (index < longValues.length) {
+                    extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof double[]) {
+                double[] doubleValues = (double[]) firstElementValue;
+                if (index < doubleValues.length) {
+                    extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof boolean[]) {
+                boolean[] booleanValues = (boolean[]) firstElementValue;
+                if (index < booleanValues.length) {
+                    extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof List) {
+                @SuppressWarnings("unchecked")
+                List<Bundle> bundles = (List<Bundle>) firstElementValue;
+                if (index < bundles.size()) {
+                    extractedValue = bundles.subList(index, index + 1);
+                }
+            } else if (firstElementValue instanceof Parcelable[]) {
+                // Special optimization: to avoid creating new singleton arrays for traversing paths
+                // we return the bare document Bundle in this particular case.
+                Parcelable[] bundles = (Parcelable[]) firstElementValue;
+                if (index < bundles.length) {
+                    extractedValue = (Bundle) bundles[index];
+                }
+            } else {
+                throw new IllegalStateException("Unsupported value type: " + firstElementValue);
+            }
+            firstElementValue = extractedValue;
+        }
+
+        // If we are at the end of the path or there are no deeper elements in this document, we
+        // have nothing to recurse into.
+        if (firstElementValue == null || remainingPath == null) {
+            return firstElementValue;
+        }
+
+        // More of the path remains; recursively evaluate it
+        if (firstElementValue instanceof Bundle) {
+            return getRawPropertyFromRawDocument(remainingPath, (Bundle) firstElementValue);
+        } else if (firstElementValue instanceof Parcelable[]) {
+            Parcelable[] parcelables = (Parcelable[]) firstElementValue;
+            if (parcelables.length == 1) {
+                return getRawPropertyFromRawDocument(remainingPath, (Bundle) parcelables[0]);
+            }
+
+            // Slowest path: we're collecting values across repeated nested docs. (Example: given a
+            // path like recipient.name, where recipient is a repeated field, we return a string
+            // array where each recipient's name is an array element).
+            //
+            // Performance note: Suppose that we have a property path "a.b.c" where the "a"
+            // property has N document values and each containing a "b" property with M document
+            // values and each of those containing a "c" property with an int array.
+            //
+            // We'll allocate a new ArrayList for each of the "b" properties, add the M int arrays
+            // from the "c" properties to it and then we'll allocate an int array in
+            // flattenAccumulator before returning that (1 + M allocation per "b" property).
+            //
+            // When we're on the "a" properties, we'll allocate an ArrayList and add the N
+            // flattened int arrays returned from the "b" properties to the list. Then we'll
+            // allocate an int array in flattenAccumulator (1 + N ("b" allocs) allocations per "a").
+            // So this implementation could incur 1 + N + NM allocs.
+            //
+            // However, we expect the vast majority of getProperty calls to be either for direct
+            // property names (not paths) or else property paths returned from snippetting, which
+            // always refer to exactly one property value and don't aggregate across repeated
+            // values. The implementation is optimized for these two cases, requiring no additional
+            // allocations. So we've decided that the above performance characteristics are OK for
+            // the less used path.
+            List<Object> accumulator = new ArrayList<>(parcelables.length);
+            for (int i = 0; i < parcelables.length; i++) {
+                Object value =
+                        getRawPropertyFromRawDocument(remainingPath, (Bundle) parcelables[i]);
+                if (value != null) {
+                    accumulator.add(value);
+                }
+            }
+            return flattenAccumulator(accumulator);
+        } else {
+            Log.e(TAG, "Failed to apply path to document; no nested value found: " + path);
+            return null;
+        }
+    }
+
+    /**
+     * Combines accumulated repeated properties from multiple documents into a single array.
+     *
+     * @param accumulator List containing objects of the following types: {@code String[]},
+     *                    {@code long[]}, {@code double[]}, {@code boolean[]}, {@code List<Bundle>},
+     *                    or {@code Parcelable[]}.
+     * @return The result of concatenating each individual list element into a larger array/list of
+     *         the same type.
+     */
+    @Nullable
+    private static Object flattenAccumulator(@NonNull List<Object> accumulator) {
+        if (accumulator.isEmpty()) {
+            return null;
+        }
+        Object first = accumulator.get(0);
+        if (first instanceof String[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((String[]) accumulator.get(i)).length;
+            }
+            String[] result = new String[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                String[] castValue = (String[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof long[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((long[]) accumulator.get(i)).length;
+            }
+            long[] result = new long[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                long[] castValue = (long[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof double[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((double[]) accumulator.get(i)).length;
+            }
+            double[] result = new double[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                double[] castValue = (double[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof boolean[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((boolean[]) accumulator.get(i)).length;
+            }
+            boolean[] result = new boolean[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                boolean[] castValue = (boolean[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof List) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((List<?>) accumulator.get(i)).size();
+            }
+            List<Bundle> result = new ArrayList<>(length);
+            for (int i = 0; i < accumulator.size(); i++) {
+                @SuppressWarnings("unchecked")
+                List<Bundle> castValue = (List<Bundle>) accumulator.get(i);
+                result.addAll(castValue);
+            }
+            return result;
+        }
+        if (first instanceof Parcelable[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((Parcelable[]) accumulator.get(i)).length;
+            }
+            Parcelable[] result = new Parcelable[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                Parcelable[] castValue = (Parcelable[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        throw new IllegalStateException("Unexpected property type: " + first);
+    }
+
+    /**
+     * Retrieves a {@link String} property by path.
+     *
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@link String} associated with the given path or {@code null} if there is
+     * no such value or the value is of a different type.
+     */
+    @Nullable
+    public String getPropertyString(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        String[] propertyArray = getPropertyStringArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return null;
         }
-        warnIfSinglePropertyTooLong("String", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("String", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code long} value by key.
+     * Retrieves a {@code long} property by path.
      *
-     * @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.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code long} associated with the given path or default value {@code 0} if
+     * there is no such value or the value is of a different type.
      */
-    public long getPropertyLong(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        long[] propertyArray = getPropertyLongArray(key);
+    public long getPropertyLong(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        long[] propertyArray = getPropertyLongArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return 0;
         }
-        warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Long", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code double} value by key.
+     * Retrieves a {@code double} property by path.
      *
-     * @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.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code double} associated with the given path or default value {@code 0.0}
+     * if there is no such value or the value is of a different type.
      */
-    public double getPropertyDouble(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        double[] propertyArray = getPropertyDoubleArray(key);
+    public double getPropertyDouble(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        double[] propertyArray = getPropertyDoubleArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return 0.0;
         }
-        warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Double", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code boolean} value by key.
+     * Retrieves a {@code boolean} property by path.
      *
-     * @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.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code boolean} associated with the given path or default value
+     * {@code false} if there is no such value or the value is of a different type.
      */
-    public boolean getPropertyBoolean(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        boolean[] propertyArray = getPropertyBooleanArray(key);
+    public boolean getPropertyBoolean(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        boolean[] propertyArray = getPropertyBooleanArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return false;
         }
-        warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Boolean", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code byte[]} value by key.
+     * Retrieves a {@code byte[]} property by path.
      *
-     * @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.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code byte[]} associated with the given path or {@code null} if there is
+     * no such value or the value is of a different type.
      */
     @Nullable
-    public byte[] getPropertyBytes(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        byte[][] propertyArray = getPropertyBytesArray(key);
+    public byte[] getPropertyBytes(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        byte[][] propertyArray = getPropertyBytesArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return null;
         }
-        warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("ByteArray", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@link GenericDocument} value by key.
+     * Retrieves a {@link GenericDocument} property by path.
      *
-     * @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.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@link GenericDocument} associated with the given path or {@code null} if
+     * there is no such value or the value is of a different type.
      */
     @Nullable
-    public GenericDocument getPropertyDocument(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        GenericDocument[] propertyArray = getPropertyDocumentArray(key);
+    public GenericDocument getPropertyDocument(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        GenericDocument[] propertyArray = getPropertyDocumentArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return null;
         }
-        warnIfSinglePropertyTooLong("Document", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Document", path, 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) {
+            @NonNull String propertyType, @NonNull String path, int propertyLength) {
         if (propertyLength > 1) {
-            Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength
+            Log.w(TAG, "The value for \"" + path + "\" contains " + propertyLength
                     + " elements. Only the first one will be returned from "
                     + "getProperty" + propertyType + "(). Try getProperty" + propertyType
                     + "Array().");
@@ -342,158 +751,168 @@
     }
 
     /**
-     * Retrieves a repeated {@code String} property by key.
+     * Retrieves a repeated {@code String} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code String[]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code String[]} associated with the given path, 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);
+    public String[] getPropertyStringArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, String[].class);
     }
 
     /**
-     * Retrieves a repeated {@link String} property by key.
+     * Retrieves a repeated {@code long[]} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code long[]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code long[]} associated with the given path, 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);
+    public long[] getPropertyLongArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, long[].class);
     }
 
     /**
-     * Retrieves a repeated {@code double} property by key.
+     * Retrieves a repeated {@code double} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code double[]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code double[]} associated with the given path, 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);
+    public double[] getPropertyDoubleArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, double[].class);
     }
 
     /**
-     * Retrieves a repeated {@code boolean} property by key.
+     * Retrieves a repeated {@code boolean} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code boolean[]} associated with the given key, or {@code null} if no value
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code boolean[]} associated with the given path, 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);
+    public boolean[] getPropertyBooleanArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, boolean[].class);
     }
 
     /**
-     * Retrieves a {@code byte[][]} property by key.
+     * Retrieves a {@code byte[][]} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code byte[][]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code byte[][]} associated with the given path, 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;
+    public byte[][] getPropertyBytesArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, byte[][].class);
     }
 
     /**
-     * Retrieves a repeated {@link GenericDocument} property by key.
+     * Retrieves a repeated {@link GenericDocument} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@link GenericDocument}[] associated with the given key, or {@code null} if no
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@link GenericDocument}[] associated with the given path, 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;
+    public GenericDocument[] getPropertyDocumentArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, GenericDocument[].class);
     }
 
     /**
-     * Gets a repeated property of the given key, and casts it to the given class type, which
-     * must be an array class type.
+     * Casts a repeated property to the provided type, logging an error and returning {@code null}
+     * if the cast fails.
+     *
+     * @param path Path to the property within the document. Used for logging.
+     * @param value Value of the property
+     * @param tClass Class to cast the value into
      */
     @Nullable
-    private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
-        Object value = mProperties.get(key);
+    private static <T> T safeCastProperty(
+            @NonNull String path, @Nullable Object value, @NonNull Class<T> tClass) {
         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);
+            Log.w(TAG, "Error casting to requested type for path \"" + path + "\"", e);
             return null;
         }
     }
 
 // @exportToFramework:startStrip()
+
     /**
-     * Converts this GenericDocument into an instance of the provided data class.
+     * Converts this GenericDocument into an instance of the provided document class.
      *
-     * <p>It is the developer's responsibility to ensure the right kind of data class is being
+     * <p>It is the developer's responsibility to ensure the right kind of document class is being
      * supplied here, either by structuring the application code to ensure the document type is
      * known, or by checking the return value of {@link #getSchemaType}.
      *
-     * <p>Document properties are identified by String keys and any that are found are assigned into
-     * fields of the given data class, so the most likely outcome of supplying the wrong data class
-     * would be an empty or partially populated result.
+     * <p>Document properties are identified by {@code String} names. Any that are found are
+     * assigned into fields of the given document class. As such, the most likely outcome of
+     * supplying the wrong document class would be an empty or partially populated result.
      *
-     * @param dataClass a class annotated with
-     *                  {@link androidx.appsearch.annotation.AppSearchDocument}.
+     * @param documentClass a class annotated with {@link Document}
+     * @return an instance of the document class after being converted from a
+     * {@link GenericDocument}
+     * @throws AppSearchException if no factory for this document class could be found on the
+     *                            classpath.
+     * @see GenericDocument#fromDocumentClass
      */
     @NonNull
-    public <T> T toDataClass(@NonNull Class<T> dataClass) throws AppSearchException {
-        Preconditions.checkNotNull(dataClass);
-        DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-        DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
+    public <T> T toDocumentClass(@NonNull Class<T> documentClass) throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+        DocumentClassFactory<T> factory = registry.getOrCreateFactory(documentClass);
         return factory.fromGenericDocument(this);
     }
 // @exportToFramework:endStrip()
 
+    /**
+     * Copies the contents of this {@link GenericDocument} into a new
+     * {@link GenericDocument.Builder}.
+     *
+     * <p>The returned builder is a deep copy whose data is separate from this document.
+     * <!--@exportToFramework:hide-->
+     */
+    // TODO(b/171882200): Expose this API in Android T
+    @NonNull
+    public GenericDocument.Builder<GenericDocument.Builder<?>> toBuilder() {
+        Bundle clonedBundle = BundleUtil.deepCopy(mBundle);
+        return new GenericDocument.Builder<>(clonedBundle);
+    }
+
     @Override
     public boolean equals(@Nullable Object other) {
         if (this == other) {
@@ -517,64 +936,100 @@
     @Override
     @NonNull
     public String toString() {
-        return bundleToString(mBundle).toString();
+        IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
+        appendGenericDocumentString(stringBuilder);
+        return stringBuilder.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(" } ");
+    /**
+     * Appends a debug string for the {@link GenericDocument} instance to the given string builder.
+     *
+     * @param builder     the builder to append to.
+     */
+    void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) {
+        Preconditions.checkNotNull(builder);
+
+        builder.append("{\n");
+        builder.increaseIndentLevel();
+
+        builder.append("namespace: \"").append(getNamespace()).append("\",\n");
+        builder.append("id: \"").append(getId()).append("\",\n");
+        builder.append("score: ").append(getScore()).append(",\n");
+        builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+        builder
+                .append("creationTimestampMillis: ")
+                .append(getCreationTimestampMillis())
+                .append(",\n");
+        builder.append("timeToLiveMillis: ").append(getTtlMillis()).append(",\n");
+
+        builder.append("properties: {\n");
+
+        String[] sortedProperties = getPropertyNames().toArray(new String[0]);
+        Arrays.sort(sortedProperties);
+
+        for (int i = 0; i < sortedProperties.length; i++) {
+            Object property = getProperty(sortedProperties[i]);
+            builder.increaseIndentLevel();
+            appendPropertyString(sortedProperties[i], property, builder);
+            if (i != sortedProperties.length - 1) {
+                builder.append(",\n");
             }
-        } 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>");
+            builder.decreaseIndentLevel();
         }
-        return stringBuilder;
+
+        builder.append("\n");
+        builder.append("}");
+
+        builder.decreaseIndentLevel();
+        builder.append("\n");
+        builder.append("}");
+    }
+
+    /**
+     * Appends a debug string for the given document property to the given string builder.
+     *
+     * @param propertyName  name of property to create string for.
+     * @param property      property object to create string for.
+     * @param builder       the builder to append to.
+     */
+    private void appendPropertyString(@NonNull String propertyName, @NonNull Object property,
+            @NonNull IndentingStringBuilder builder) {
+        Preconditions.checkNotNull(propertyName);
+        Preconditions.checkNotNull(property);
+        Preconditions.checkNotNull(builder);
+
+        builder.append("\"").append(propertyName).append("\": [");
+        if (property instanceof GenericDocument[]) {
+            GenericDocument[] documentValues = (GenericDocument[]) property;
+            for (int i = 0; i < documentValues.length; ++i) {
+                builder.append("\n");
+                builder.increaseIndentLevel();
+                documentValues[i].appendGenericDocumentString(builder);
+                if (i != documentValues.length - 1) {
+                    builder.append(",");
+                }
+                builder.append("\n");
+                builder.decreaseIndentLevel();
+            }
+            builder.append("]");
+        } else {
+            int propertyArrLength = Array.getLength(property);
+            for (int i = 0; i < propertyArrLength; i++) {
+                Object propertyElement = Array.get(property, i);
+                if (propertyElement instanceof String) {
+                    builder.append("\"").append((String) propertyElement).append("\"");
+                } else if (propertyElement instanceof byte[]) {
+                    builder.append(Arrays.toString((byte[]) propertyElement));
+                } else {
+                    builder.append(propertyElement.toString());
+                }
+                if (i != propertyArrLength - 1) {
+                    builder.append(", ");
+                } else {
+                    builder.append("]");
+                }
+            }
+        }
     }
 
     /**
@@ -586,73 +1041,126 @@
     // GenericDocument.
     @SuppressLint("StaticFinalBuilder")
     public static class Builder<BuilderType extends Builder> {
-
-        private final Bundle mProperties = new Bundle();
-        private final Bundle mBundle = new Bundle();
+        private Bundle mBundle;
+        private Bundle mProperties;
         private final BuilderType mBuilderTypeInstance;
         private boolean mBuilt = false;
 
         /**
-         * Create a new {@link GenericDocument.Builder}.
+         * Creates 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
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace  the namespace to set for the {@link GenericDocument}.
+         * @param id         the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *                   provided {@code schemaType} must be defined using
          *                   {@link AppSearchSession#setSchema} prior
          *                   to inserting a document of this {@code schemaType} into the
          *                   AppSearch index using
-         *                   {@link AppSearchSession#putDocuments}. Otherwise, the document will be
-         *                   rejected by {@link AppSearchSession#putDocuments}.
+         *                   {@link AppSearchSession#put}.
+         *                   Otherwise, the document will be rejected by
+         *                   {@link AppSearchSession#put} with result code
+         *                   {@link AppSearchResult#RESULT_NOT_FOUND}.
          */
         @SuppressWarnings("unchecked")
-        public Builder(@NonNull String uri, @NonNull String schemaType) {
-            Preconditions.checkNotNull(uri);
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+            Preconditions.checkNotNull(namespace);
+            Preconditions.checkNotNull(id);
             Preconditions.checkNotNull(schemaType);
+
+            mBundle = new Bundle();
             mBuilderTypeInstance = (BuilderType) this;
-            mBundle.putString(GenericDocument.URI_FIELD, uri);
+            mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
+            mBundle.putString(GenericDocument.ID_FIELD, id);
             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);
+
+            mProperties = new Bundle();
             mBundle.putBundle(PROPERTIES_FIELD, mProperties);
         }
 
         /**
-         * Sets the app-defined namespace this Document resides in. No special values are
-         * reserved or understood by the infrastructure.
+         * Creates a new {@link GenericDocument.Builder} from the given Bundle.
          *
-         * <p>URIs are unique within a namespace.
+         * <p>The bundle is NOT copied.
+         */
+        @SuppressWarnings("unchecked")
+        Builder(@NonNull Bundle bundle) {
+            mBundle = Preconditions.checkNotNull(bundle);
+            mProperties = mBundle.getBundle(PROPERTIES_FIELD);
+            mBuilderTypeInstance = (BuilderType) this;
+        }
+
+        /**
+         * Sets the app-defined namespace this document resides in, changing the value provided
+         * in the constructor. No special values are reserved or understood by the infrastructure.
+         *
+         * <p>Document IDs are unique within a namespace.
          *
          * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         * <!--@exportToFramework:hide-->
          */
         @NonNull
         public BuilderType setNamespace(@NonNull String namespace) {
+            Preconditions.checkNotNull(namespace);
+            resetIfBuilt();
             mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
             return mBuilderTypeInstance;
         }
 
         /**
+         * Sets the ID of this document, changing the value provided in the constructor. No
+         * special values are reserved or understood by the infrastructure.
+         *
+         * <p>Document IDs are unique within a namespace.
+         * <!--@exportToFramework:hide-->
+         */
+        @NonNull
+        public BuilderType setId(@NonNull String id) {
+            Preconditions.checkNotNull(id);
+            resetIfBuilt();
+            mBundle.putString(GenericDocument.ID_FIELD, id);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets the schema type of this document, changing the value provided in the constructor.
+         *
+         * <p>To successfully index a document, the schema type must match the name of an
+         * {@link AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}.
+         * <!--@exportToFramework:hide-->
+         */
+        @NonNull
+        public BuilderType setSchemaType(@NonNull String schemaType) {
+            Preconditions.checkNotNull(schemaType);
+            resetIfBuilt();
+            mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
+            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.
+         * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
          *
          * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
          * Documents with higher scores are considered better than documents with lower scores.
          *
-         * <p>Any nonnegative integer can be used a score.
+         * <p>Any non-negative integer can be used a score. By default, scores are set to 0.
          *
-         * @throws IllegalArgumentException If the provided value is negative.
+         * @param score any non-negative {@code int} representing the document's score.
          */
         @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.");
             }
+            resetIfBuilt();
             mBundle.putInt(GenericDocument.SCORE_FIELD, score);
             return mBuilderTypeInstance;
         }
@@ -660,36 +1168,41 @@
         /**
          * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
          *
-         * <p>Should be set using a value obtained from the {@link System#currentTimeMillis} time
-         * base.
+         * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
+         * time base.
+         *
+         * <p>If this method is not called, this will be set to the time the object is built.
+         *
+         * @param creationTimestampMillis a creation timestamp in milliseconds.
          */
         @NonNull
-        public BuilderType setCreationTimestampMillis(long creationTimestampMillis) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
-                    creationTimestampMillis);
+        public BuilderType setCreationTimestampMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
+            resetIfBuilt();
+            mBundle.putLong(
+                    GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, creationTimestampMillis);
             return mBuilderTypeInstance;
         }
 
         /**
-         * Sets the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
+         * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
          *
          * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
          * {@code creationTimestampMillis + ttlMillis}, measured in the
          * {@link System#currentTimeMillis} time base, the document will be auto-deleted.
          *
          * <p>The default value is 0, which means the document is permanent and won't be
-         * auto-deleted until the app is uninstalled.
+         * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is
+         * called.
          *
-         * @param ttlMillis A non-negative duration in milliseconds.
-         * @throws IllegalArgumentException If the provided value is negative.
+         * @param ttlMillis a non-negative duration in milliseconds.
          */
         @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.");
             }
+            resetIfBuilt();
             mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis);
             return mBuilderTypeInstance;
         }
@@ -698,15 +1211,19 @@
          * 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.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code String} values of the property.
+         * @throws IllegalArgumentException if no values are provided, or if a passed in
+         *                                  {@code String} is {@code null}.
          */
         @NonNull
-        public BuilderType setPropertyString(@NonNull String key, @NonNull String... values) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+        public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) {
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            resetIfBuilt();
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -714,15 +1231,17 @@
          * 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.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code boolean} values of the property.
          */
         @NonNull
-        public BuilderType setPropertyBoolean(@NonNull String key, @NonNull boolean... values) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+        public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) {
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            resetIfBuilt();
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -730,15 +1249,17 @@
          * 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.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code long} values of the property.
          */
         @NonNull
-        public BuilderType setPropertyLong(@NonNull String key, @NonNull long... values) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+        public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) {
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            resetIfBuilt();
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -746,30 +1267,36 @@
          * 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.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code double} values of the property.
          */
         @NonNull
-        public BuilderType setPropertyDouble(@NonNull String key, @NonNull double... values) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+        public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) {
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            resetIfBuilt();
+            putInPropertyBundle(name, 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.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code byte[]} of the property.
+         * @throws IllegalArgumentException if no values are provided, or if a passed in
+         *                                  {@code byte[]} is {@code null}.
          */
         @NonNull
-        public BuilderType setPropertyBytes(@NonNull String key, @NonNull byte[]... values) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+        public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) {
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            resetIfBuilt();
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -777,47 +1304,59 @@
          * 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.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@link GenericDocument} values of the property.
+         * @throws IllegalArgumentException if no values are provided, or if a passed in
+         *                                  {@link GenericDocument} is {@code null}.
          */
         @NonNull
         public BuilderType setPropertyDocument(
-                @NonNull String key, @NonNull GenericDocument... values) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+                @NonNull String name, @NonNull GenericDocument... values) {
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            resetIfBuilt();
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull String[] values)
+        /**
+         * Clears the value for the property with the given name.
+         *
+         * <p>Note that this method does not support property paths.
+         *
+         * @param name The name of the property to clear.
+         * <!--@exportToFramework:hide-->
+         */
+        @NonNull
+        public BuilderType clearProperty(@NonNull String name) {
+            Preconditions.checkNotNull(name);
+            resetIfBuilt();
+            mProperties.remove(name);
+            return mBuilderTypeInstance;
+        }
+
+        private void putInPropertyBundle(@NonNull String name, @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);
+            mProperties.putStringArray(name, values);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull boolean[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.putBooleanArray(key, values);
+        private void putInPropertyBundle(@NonNull String name, @NonNull boolean[] values) {
+            mProperties.putBooleanArray(name, values);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull double[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.putDoubleArray(key, values);
+        private void putInPropertyBundle(@NonNull String name, @NonNull double[] values) {
+            mProperties.putDoubleArray(name, values);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull long[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.putLongArray(key, values);
+        private void putInPropertyBundle(@NonNull String name, @NonNull long[] values) {
+            mProperties.putLongArray(name, values);
         }
 
         /**
@@ -826,8 +1365,7 @@
          * <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);
+        private void putInPropertyBundle(@NonNull String name, @NonNull byte[][] values) {
             ArrayList<Bundle> bundles = new ArrayList<>(values.length);
             for (int i = 0; i < values.length; i++) {
                 if (values[i] == null) {
@@ -837,38 +1375,38 @@
                 bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);
                 bundles.add(bundle);
             }
-            mProperties.putParcelableArrayList(key, bundles);
+            mProperties.putParcelableArrayList(name, bundles);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull GenericDocument[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            Bundle[] documentBundles = new Bundle[values.length];
+        private void putInPropertyBundle(@NonNull String name, @NonNull GenericDocument[] values) {
+            Parcelable[] documentBundles = new Parcelable[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);
-            }
+            mProperties.putParcelableArray(name, documentBundles);
         }
 
         /** Builds the {@link GenericDocument} object. */
         @NonNull
         public GenericDocument build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBuilt = true;
+            // Set current timestamp for creation timestamp by default.
+            if (mBundle.getLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, -1) == -1) {
+                mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
+                        System.currentTimeMillis());
+            }
             return new GenericDocument(mBundle);
         }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mBundle = BundleUtil.deepCopy(mBundle);
+                mProperties = mBundle.getBundle(PROPERTIES_FIELD);
+                mBuilt = false;
+            }
+        }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
new file mode 100644
index 0000000..d9bfc0d
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -0,0 +1,181 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Encapsulates a request to retrieve documents by namespace and IDs from the
+ * {@link AppSearchSession} database.
+ *
+ * @see AppSearchSession#getByDocumentId
+ */
+public final class GetByDocumentIdRequest {
+    /**
+     * Schema type to be used in
+     * {@link GetByDocumentIdRequest.Builder#addProjection}
+     * to apply property paths to all results, excepting any types that have had their own, specific
+     * property paths set.
+     */
+    public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+    private final String mNamespace;
+    private final Set<String> mIds;
+    private final Map<String, List<String>> mTypePropertyPathsMap;
+
+    GetByDocumentIdRequest(@NonNull String namespace, @NonNull Set<String> ids, @NonNull Map<String,
+            List<String>> typePropertyPathsMap) {
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mIds = Preconditions.checkNotNull(ids);
+        mTypePropertyPathsMap = Preconditions.checkNotNull(typePropertyPathsMap);
+    }
+
+    /** Returns the namespace attached to the request. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the set of document IDs attached to the request. */
+    @NonNull
+    public Set<String> getIds() {
+        return Collections.unmodifiableSet(mIds);
+    }
+
+    /**
+     * Returns a map from schema type to property paths to be used for projection.
+     *
+     * <p>If the map is empty, then all properties will be retrieved for all results.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
+     * by this function, rather than calling it multiple times.
+     */
+    @NonNull
+    public Map<String, List<String>> getProjections() {
+        Map<String, List<String>> copy = new ArrayMap<>();
+        for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
+            copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+        }
+        return copy;
+    }
+
+    /**
+     * Returns a map from schema type to property paths to be used for projection.
+     *
+     * <p>If the map is empty, then all properties will be retrieved for all results.
+     *
+     * <p>A more efficient version of {@link #getProjections}, but it returns a modifiable map.
+     * This is not meant to be unhidden and should only be used by internal classes.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Map<String, List<String>> getProjectionsInternal() {
+        return mTypePropertyPathsMap;
+    }
+
+    /** Builder for {@link GetByDocumentIdRequest} objects. */
+    public static final class Builder {
+        private final String mNamespace;
+        private ArraySet<String> mIds = new ArraySet<>();
+        private ArrayMap<String, List<String>> mProjectionTypePropertyPaths = new ArrayMap<>();
+        private boolean mBuilt = false;
+
+        /** Creates a {@link GetByDocumentIdRequest.Builder} instance. */
+        public Builder(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+        }
+
+        /** Adds one or more document IDs to the request. */
+        @NonNull
+        public Builder addIds(@NonNull String... ids) {
+            Preconditions.checkNotNull(ids);
+            resetIfBuilt();
+            return addIds(Arrays.asList(ids));
+        }
+
+        /** Adds a collection of IDs to the request. */
+        @NonNull
+        public Builder addIds(@NonNull Collection<String> ids) {
+            Preconditions.checkNotNull(ids);
+            resetIfBuilt();
+            mIds.addAll(ids);
+            return this;
+        }
+
+        /**
+         * Adds property paths for the specified type to be used for projection. If property
+         * paths are added for a type, then only the properties referred to will be retrieved for
+         * results of that type. If a property path that is specified isn't present in a result,
+         * it will be ignored for that result. Property paths cannot be null.
+         *
+         * <p>If no property paths are added for a particular type, then all properties of
+         * results of that type will be retrieved.
+         *
+         * <p>If property path is added for the
+         * {@link GetByDocumentIdRequest#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths
+         * will apply to all results, excepting any types that have their own, specific property
+         * paths set.
+         *
+         * @see SearchSpec.Builder#addProjection
+         */
+        @NonNull
+        public Builder addProjection(
+                @NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(propertyPaths);
+            resetIfBuilt();
+            List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+            for (String propertyPath : propertyPaths) {
+                Preconditions.checkNotNull(propertyPath);
+                propertyPathsList.add(propertyPath);
+            }
+            mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);
+            return this;
+        }
+
+        /** Builds a new {@link GetByDocumentIdRequest}. */
+        @NonNull
+        public GetByDocumentIdRequest build() {
+            mBuilt = true;
+            return new GetByDocumentIdRequest(mNamespace, mIds, mProjectionTypePropertyPaths);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mIds = new ArraySet<>(mIds);
+                // No need to clone each propertyPathsList inside mProjectionTypePropertyPaths since
+                // the builder only replaces it, never adds to it. So even if the builder is used
+                // again, the previous one will remain with the object.
+                mProjectionTypePropertyPaths = new ArrayMap<>(mProjectionTypePropertyPaths);
+                mBuilt = false;
+            }
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByUriRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByUriRequest.java
index 9461790..727b2a1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByUriRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByUriRequest.java
@@ -17,81 +17,187 @@
 package androidx.appsearch.app;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
- * Encapsulates a request to retrieve documents by namespace and URI.
- *
- * @see AppSearchSession#getByUri
+ * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+ * @hide
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Deprecated
 public final class GetByUriRequest {
+    /**
+     * Schema type to be used in
+     * {@link GetByUriRequest.Builder#addProjection}
+     * to apply property paths to all results, excepting any types that have had their own, specific
+     * property paths set.
+     */
+    public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     private final String mNamespace;
-    private final Set<String> mUris;
+    private final Set<String> mIds;
+    private final Map<String, List<String>> mTypePropertyPathsMap;
 
-    GetByUriRequest(@NonNull String namespace, @NonNull Set<String> uris) {
-        mNamespace = namespace;
-        mUris = uris;
+    GetByUriRequest(@NonNull String namespace, @NonNull Set<String> ids, @NonNull Map<String,
+            List<String>> typePropertyPathsMap) {
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mIds = Preconditions.checkNotNull(ids);
+        mTypePropertyPathsMap = Preconditions.checkNotNull(typePropertyPathsMap);
     }
 
-    /** Returns the namespace to get documents from. */
+    /** Returns the namespace attached to the request. */
     @NonNull
     public String getNamespace() {
         return mNamespace;
     }
 
-    /** Returns the URIs to get from the namespace. */
+    /** Returns the set of document IDs attached to the request. */
     @NonNull
     public Set<String> getUris() {
-        return Collections.unmodifiableSet(mUris);
+        return Collections.unmodifiableSet(mIds);
     }
 
-    /** Builder for {@link GetByUriRequest} objects. */
+    /**
+     * Returns a map from schema type to property paths to be used for projection.
+     *
+     * <p>If the map is empty, then all properties will be retrieved for all results.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
+     * by this function, rather than calling it multiple times.
+     */
+    @NonNull
+    public Map<String, List<String>> getProjections() {
+        Map<String, List<String>> copy = new ArrayMap<>();
+        for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
+            copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+        }
+        return copy;
+    }
+
+    /**
+     * Returns a map from schema type to property paths to be used for projection.
+     *
+     * <p>If the map is empty, then all properties will be retrieved for all results.
+     *
+     * <p>A more efficient version of {@link #getProjections}, but it returns a modifiable map.
+     * This is not meant to be unhidden and should only be used by internal classes.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Map<String, List<String>> getProjectionsInternal() {
+        return mTypePropertyPathsMap;
+    }
+
+    /**
+     * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Deprecated
+    @NonNull
+    public GetByDocumentIdRequest toGetByDocumentIdRequest() {
+        GetByDocumentIdRequest.Builder builder = new GetByDocumentIdRequest.Builder(mNamespace)
+                .addIds(mIds);
+        for (Map.Entry<String, List<String>> projection : mTypePropertyPathsMap.entrySet()) {
+            builder.addProjection(projection.getKey(), projection.getValue());
+        }
+        return builder.build();
+    }
+
+    /**
+     * Builder for {@link GetByUriRequest} objects.
+     *
+     * <p>Once {@link #build} is called, the instance can no longer be used.
+     */
     public static final class Builder {
-        private String mNamespace = GenericDocument.DEFAULT_NAMESPACE;
-        private final Set<String> mUris = new ArraySet<>();
+        private final String mNamespace;
+        private final Set<String> mIds = new ArraySet<>();
+        private final Map<String, List<String>> mProjectionTypePropertyPaths = new ArrayMap<>();
         private boolean mBuilt = false;
 
         /**
-         * Sets which namespace these documents will be retrieved from.
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        public Builder(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+        }
+
+        /**
+         * Adds one or more document IDs to the request.
          *
-         * <p>If this is not set, it defaults to {@link GenericDocument#DEFAULT_NAMESPACE}.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
-        public Builder setNamespace(@NonNull String namespace) {
+        public Builder addUris(@NonNull String... ids) {
+            Preconditions.checkNotNull(ids);
+            return addUris(Arrays.asList(ids));
+        }
+
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        @NonNull
+        public Builder addUris(@NonNull Collection<String> ids) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(namespace);
-            mNamespace = namespace;
+            Preconditions.checkNotNull(ids);
+            mIds.addAll(ids);
             return this;
         }
 
-        /** Adds one or more URIs to the request. */
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
         @NonNull
-        public Builder addUri(@NonNull String... uris) {
-            Preconditions.checkNotNull(uris);
-            return addUri(Arrays.asList(uris));
-        }
-
-        /** Adds one or more URIs to the request. */
-        @NonNull
-        public Builder addUri(@NonNull Collection<String> uris) {
+        public Builder addProjection(
+                @NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(uris);
-            mUris.addAll(uris);
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(propertyPaths);
+            List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+            for (String propertyPath : propertyPaths) {
+                Preconditions.checkNotNull(propertyPath);
+                propertyPathsList.add(propertyPath);
+            }
+            mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);
             return this;
         }
 
-        /** Builds a new {@link GetByUriRequest}. */
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
         @NonNull
         public GetByUriRequest build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBuilt = true;
-            return new GetByUriRequest(mNamespace, mUris);
+            return new GetByUriRequest(mNamespace, mIds, mProjectionTypePropertyPaths);
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
new file mode 100644
index 0000000..03c1f21
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import android.os.Bundle;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/** The response class of {@link AppSearchSession#getSchema} */
+public final class GetSchemaResponse {
+    private static final String VERSION_FIELD = "version";
+    private static final String SCHEMAS_FIELD = "schemas";
+
+    private final Bundle mBundle;
+
+    GetSchemaResponse(@NonNull Bundle bundle) {
+        mBundle = Preconditions.checkNotNull(bundle);
+    }
+
+    /**
+     * Returns the {@link Bundle} populated by this builder.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Returns the overall database schema version.
+     *
+     * <p>If the database is empty, 0 will be returned.
+     */
+    @IntRange(from = 0)
+    public int getVersion() {
+        return mBundle.getInt(VERSION_FIELD);
+    }
+
+    /**
+     * Return the schemas most recently successfully provided to
+     * {@link AppSearchSession#setSchema}.
+     *
+     * <p>It is inefficient to call this method repeatedly.
+     */
+    @NonNull
+    public Set<AppSearchSchema> getSchemas() {
+        ArrayList<Bundle> schemaBundles = mBundle.getParcelableArrayList(SCHEMAS_FIELD);
+        Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
+        for (int i = 0; i < schemaBundles.size(); i++) {
+            schemas.add(new AppSearchSchema(schemaBundles.get(i)));
+        }
+        return schemas;
+    }
+
+    /** Builder for {@link GetSchemaResponse} objects. */
+    public static final class Builder {
+        private int mVersion = 0;
+        private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
+        private boolean mBuilt = false;
+
+        /**
+         * Sets the database overall schema version.
+         *
+         * <p>Default version is 0
+         */
+        @NonNull
+        public Builder setVersion(@IntRange(from = 0) int version) {
+            resetIfBuilt();
+            mVersion = version;
+            return this;
+        }
+
+        /**  Adds one {@link AppSearchSchema} to the schema list.  */
+        @NonNull
+        public Builder addSchema(@NonNull AppSearchSchema schema) {
+            Preconditions.checkNotNull(schema);
+            resetIfBuilt();
+            mSchemaBundles.add(schema.getBundle());
+            return this;
+        }
+
+        /** Builds a {@link GetSchemaResponse} object. */
+        @NonNull
+        public GetSchemaResponse build() {
+            Bundle bundle = new Bundle();
+            bundle.putInt(VERSION_FIELD, mVersion);
+            bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles);
+            mBuilt = true;
+            return new GetSchemaResponse(bundle);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mSchemaBundles = new ArrayList<>(mSchemaBundles);
+                mBuilt = false;
+            }
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
index fc35552..8b7dbe9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -18,54 +18,66 @@
 
 import androidx.annotation.NonNull;
 
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.Closeable;
+
 /**
- * This class provides global access to the centralized AppSearch index maintained by the system.
+ * Provides a connection to all AppSearch databases the querying application has been
+ * granted access to.
  *
- * <p>Apps can retrieve indexed documents through the query API.
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see AppSearchSession
  */
-public interface GlobalSearchSession {
+public interface GlobalSearchSession extends Closeable {
     /**
-     * Searches across all documents in the storage based on a given query string.
+     * Retrieves documents from all AppSearch databases that the querying application has access to.
      *
-     * <p>Currently we support following features in the raw query format:
-     * <ul>
-     *     <li>AND
-     *     <p>AND joins (e.g. “match documents that have both the terms ‘dog’ and
-     *     ‘cat’”).
-     *     Example: hello world matches documents that have both ‘hello’ and ‘world’
-     *     <li>OR
-     *     <p>OR joins (e.g. “match documents that have either the term ‘dog’ or
-     *     ‘cat’”).
-     *     Example: dog OR puppy
-     *     <li>Exclusion
-     *     <p>Exclude a term (e.g. “match documents that do
-     *     not have the term ‘dog’”).
-     *     Example: -dog excludes the term ‘dog’
-     *     <li>Grouping terms
-     *     <p>Allow for conceptual grouping of subqueries to enable hierarchical structures (e.g.
-     *     “match documents that have either ‘dog’ or ‘puppy’, and either ‘cat’ or ‘kitten’”).
-     *     Example: (dog puppy) (cat kitten) two one group containing two terms.
-     *     <li>Property restricts
-     *     <p> Specifies which properties of a document to specifically match terms in (e.g.
-     *     “match documents where the ‘subject’ property contains ‘important’”).
-     *     Example: subject:important matches documents with the term ‘important’ in the
-     *     ‘subject’ property
-     *     <li>Schema type restricts
-     *     <p>This is similar to property restricts, but allows for restricts on top-level document
-     *     fields, such as schema_type. Clients should be able to limit their query to documents of
-     *     a certain schema_type (e.g. “match documents that are of the ‘Email’ schema_type”).
-     *     Example: { schema_type_filters: “Email”, “Video”,query: “dog” } will match documents
-     *     that contain the query term ‘dog’ and are of either the ‘Email’ schema type or the
-     *     ‘Video’ schema type.
-     * </ul>
+     * <p>Applications can be granted access to documents by specifying
+     * {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}, or
+     * {@link SetSchemaRequest.Builder#setDocumentClassVisibilityForPackage} when building a schema.
      *
-     * <p> This method is lightweight. The heavy work will be done in
-     * {@link SearchResults#getNextPage()}.
+     * <p>Document access can also be granted to system UIs by specifying
+     * {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}, or
+     * {@link SetSchemaRequest.Builder#setDocumentClassDisplayedBySystem}
+     * when building a schema.
      *
-     * @param queryExpression Query String to search.
-     * @param searchSpec      Spec for setting filters, raw query etc.
-     * @return The search result of performing this operation.
+     * <p>See {@link AppSearchSession#search} for a detailed explanation on
+     * forming a query string.
+     *
+     * <p>This method is lightweight. The heavy work will be done in
+     * {@link SearchResults#getNextPage}.
+     *
+     * @param queryExpression query string to search.
+     * @param searchSpec      spec for setting document filters, adding projection, setting term
+     *                        match type, etc.
+     * @return a {@link SearchResults} object for retrieved matched documents.
      */
     @NonNull
-    SearchResults query(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+    SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Reports that a particular document has been used from a system surface.
+     *
+     * <p>See {@link AppSearchSession#reportUsage} for a general description of document usage, as
+     * well as an API that can be used by the app itself.
+     *
+     * <p>Usage reported via this method is accounted separately from usage reported via
+     * {@link AppSearchSession#reportUsage} and may be accessed using the constants
+     * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and
+     * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
+     *
+     * @return The pending result of performing this operation which resolves to {@code null} on
+     *     success. The pending result will be completed with an
+     *     {@link androidx.appsearch.exceptions.AppSearchException} with a code of
+     *     {@link AppSearchResult#RESULT_SECURITY_ERROR} if this API is invoked by an app which
+     *     is not part of the system.
+     */
+    @NonNull
+    ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request);
+
+    /** Closes the {@link GlobalSearchSession}. */
+    @Override
+    void close();
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
new file mode 100644
index 0000000..b47735b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+
+/**
+ * A migrator class to translate {@link GenericDocument} from different version of
+ * {@link AppSearchSchema}
+ *
+ * <p>Make non-backwards-compatible changes will delete all stored documents in old schema. You
+ * can save your documents by setting {@link Migrator} via the
+ * {@link SetSchemaRequest.Builder#setMigrator} for each type and target version you want to save.
+ *
+ * <p>{@link #onDowngrade} or {@link #onUpgrade} will be triggered if the version number of the
+ * schema stored in AppSearch is different with the version in the request.
+ *
+ * <p>If any error or Exception occurred in the {@link #onDowngrade} or {@link #onUpgrade}, all the
+ * setSchema request will be rejected unless the schema changes are backwards-compatible, and stored
+ * documents won't have any observable changes.
+ */
+public abstract class Migrator {
+    /**
+     * Returns {@code true} if this migrator's source type needs to be migrated to update from
+     * currentVersion to finalVersion.
+     *
+     * <p>Migration won't be triggered if currentVersion is equal to finalVersion even if
+     * {@link #shouldMigrate} return true;
+     */
+    public abstract boolean shouldMigrate(int currentVersion, int finalVersion);
+
+    /**
+     * Migrates {@link GenericDocument} to a newer version of {@link AppSearchSchema}.
+     *
+     * <p>This method will be invoked only if the {@link SetSchemaRequest} is setting a
+     * higher version number than the current {@link AppSearchSchema} saved in AppSearch.
+     *
+     * <p>If this {@link Migrator} is provided to cover a compatible schema change via
+     * {@link AppSearchSession#setSchema}, documents under the old version won't be removed
+     * unless you use the same document ID.
+     *
+     * <p>This method will be invoked on the background worker thread provided via
+     * {@link AppSearchSession#setSchema}.
+     *
+     * @param currentVersion The current version of the document's schema.
+     * @param finalVersion  The final version that documents need to be migrated to.
+     * @param document       The {@link GenericDocument} need to be translated to new version.
+     * @return               A {@link GenericDocument} in new version.
+     */
+    @WorkerThread
+    @NonNull
+    public abstract GenericDocument onUpgrade(int currentVersion, int finalVersion,
+            @NonNull GenericDocument document);
+
+    /**
+     * Migrates {@link GenericDocument} to an older version of {@link AppSearchSchema}.
+     *
+     * <p>This method will be invoked only if the {@link SetSchemaRequest} is setting a
+     * lower version number than the current {@link AppSearchSchema} saved in AppSearch.
+     *
+     * <p>If this {@link Migrator} is provided to cover a compatible schema change via
+     * {@link AppSearchSession#setSchema}, documents under the old version won't be removed
+     * unless you use the same document ID.
+     *
+     * <p>This method will be invoked on the background worker thread.
+     *
+     * @param currentVersion The current version of the document's schema.
+     * @param finalVersion  The final version that documents need to be migrated to.
+     * @param document       The {@link GenericDocument} need to be translated to new version.
+     * @return               A {@link GenericDocument} in new version.
+     */
+    @WorkerThread
+    @NonNull
+    public abstract GenericDocument onDowngrade(int currentVersion, int finalVersion,
+            @NonNull GenericDocument document);
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
index 17d6fae..5d54f23 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
@@ -16,16 +16,19 @@
 
 package androidx.appsearch.app;
 
-import androidx.annotation.NonNull;
-import androidx.core.util.ObjectsCompat;
-import androidx.core.util.Preconditions;
+import android.os.Bundle;
 
-import java.util.Arrays;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.util.BundleUtil;
+import androidx.core.util.Preconditions;
 
 /** This class represents a uniquely identifiable package. */
 public class PackageIdentifier {
-    private final String mPackageName;
-    private final byte[] mSha256Certificate;
+    private static final String PACKAGE_NAME_FIELD = "packageName";
+    private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
+
+    private final Bundle mBundle;
 
     /**
      * Creates a unique identifier for a package.
@@ -34,18 +37,32 @@
      * @param sha256Certificate SHA256 certificate digest of the package.
      */
     public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) {
-        mPackageName = Preconditions.checkNotNull(packageName);
-        mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);
+        mBundle = new Bundle();
+        mBundle.putString(PACKAGE_NAME_FIELD, packageName);
+        mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public PackageIdentifier(@NonNull Bundle bundle) {
+        mBundle = Preconditions.checkNotNull(bundle);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Bundle getBundle() {
+        return mBundle;
     }
 
     @NonNull
     public String getPackageName() {
-        return mPackageName;
+        return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));
     }
 
     @NonNull
     public byte[] getSha256Certificate() {
-        return mSha256Certificate;
+        return Preconditions.checkNotNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD));
     }
 
     @Override
@@ -57,12 +74,11 @@
             return false;
         }
         final PackageIdentifier other = (PackageIdentifier) obj;
-        return this.mPackageName.equals(other.mPackageName)
-                && Arrays.equals(this.mSha256Certificate, other.mSha256Certificate);
+        return BundleUtil.deepEquals(mBundle, other.mBundle);
     }
 
     @Override
     public int hashCode() {
-        return ObjectsCompat.hash(mPackageName, Arrays.hashCode(mSha256Certificate));
+        return BundleUtil.deepHashCode(mBundle);
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
index 17d424e..4d4a000 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -29,9 +29,15 @@
 import java.util.List;
 
 /**
- * Encapsulates a request to index a document into an {@link AppSearchSession} database.
+ * Encapsulates a request to index documents into an {@link AppSearchSession} database.
  *
- * <p>@see AppSearchSession#putDocuments
+ * <!--@exportToFramework:ifJetpack()-->
+ * <p>Documents added to the request can be instances of classes annotated with
+ * {@link androidx.appsearch.annotation.Document} or instances of
+ * {@link GenericDocument}.
+ * <!--@exportToFramework:else()-->
+ *
+ * @see AppSearchSession#put
  */
 public final class PutDocumentsRequest {
     private final List<GenericDocument> mDocuments;
@@ -40,95 +46,90 @@
         mDocuments = documents;
     }
 
-    /** Returns the documents that are part of this request. */
+    /** Returns a list of {@link GenericDocument} objects that are part of this request. */
     @NonNull
-    public List<GenericDocument> getDocuments() {
+    public List<GenericDocument> getGenericDocuments() {
         return Collections.unmodifiableList(mDocuments);
     }
 
-    /**
-    * Builder for {@link PutDocumentsRequest} objects.
-    *
-    * <p>Once {@link #build} is called, the instance can no longer be used.
-    */
+    /** Builder for {@link PutDocumentsRequest} objects. */
     public static final class Builder {
-        private final List<GenericDocument> mDocuments = new ArrayList<>();
+        private ArrayList<GenericDocument> mDocuments = new ArrayList<>();
         private boolean mBuilt = false;
 
         /** Adds one or more {@link GenericDocument} objects to the request. */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
         @NonNull
-        public Builder addGenericDocument(@NonNull GenericDocument... documents) {
+        public Builder addGenericDocuments(@NonNull GenericDocument... documents) {
             Preconditions.checkNotNull(documents);
-            return addGenericDocument(Arrays.asList(documents));
+            resetIfBuilt();
+            return addGenericDocuments(Arrays.asList(documents));
         }
 
         /** Adds a collection of {@link GenericDocument} objects to the request. */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
         @NonNull
-        public Builder addGenericDocument(
+        public Builder addGenericDocuments(
                 @NonNull Collection<? extends GenericDocument> documents) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkNotNull(documents);
+            resetIfBuilt();
             mDocuments.addAll(documents);
             return this;
         }
 
 // @exportToFramework:startStrip()
         /**
-         * Adds one or more annotated {@link androidx.appsearch.annotation.AppSearchDocument}
+         * Adds one or more annotated {@link androidx.appsearch.annotation.Document}
          * documents to the request.
          *
-         * @param dataClasses annotated
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument} documents.
-         * @throws AppSearchException if an error occurs converting a data class into a
+         * @param documents annotated
+         *                    {@link androidx.appsearch.annotation.Document} documents.
+         * @throws AppSearchException if an error occurs converting a document class into a
          *                            {@link GenericDocument}.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
+        // Merged list available from getGenericDocuments()
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addDataClass(@NonNull Object... dataClasses) throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            return addDataClass(Arrays.asList(dataClasses));
+        public Builder addDocuments(@NonNull Object... documents) throws AppSearchException {
+            Preconditions.checkNotNull(documents);
+            resetIfBuilt();
+            return addDocuments(Arrays.asList(documents));
         }
 
         /**
          * Adds a collection of annotated
-         * {@link androidx.appsearch.annotation.AppSearchDocument} documents to the request.
+         * {@link androidx.appsearch.annotation.Document} documents to the request.
          *
-         * @param dataClasses annotated
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument} documents.
-         * @throws AppSearchException if an error occurs converting a data class into a
+         * @param documents annotated
+         *                    {@link androidx.appsearch.annotation.Document} documents.
+         * @throws AppSearchException if an error occurs converting a document into a
          *                            {@link GenericDocument}.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
+        // Merged list available from getGenericDocuments()
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addDataClass(@NonNull Collection<?> dataClasses)
-                throws AppSearchException {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(dataClasses);
-            List<GenericDocument> genericDocuments = new ArrayList<>(dataClasses.size());
-            for (Object dataClass : dataClasses) {
-                GenericDocument genericDocument = toGenericDocument(dataClass);
+        public Builder addDocuments(@NonNull Collection<?> documents) throws AppSearchException {
+            Preconditions.checkNotNull(documents);
+            resetIfBuilt();
+            List<GenericDocument> genericDocuments = new ArrayList<>(documents.size());
+            for (Object document : documents) {
+                GenericDocument genericDocument = GenericDocument.fromDocumentClass(document);
                 genericDocuments.add(genericDocument);
             }
-            return addGenericDocument(genericDocuments);
-        }
-
-        @NonNull
-        private static <T> GenericDocument toGenericDocument(@NonNull T dataClass)
-                throws AppSearchException {
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
-            return factory.toGenericDocument(dataClass);
+            return addGenericDocuments(genericDocuments);
         }
 // @exportToFramework:endStrip()
 
         /** Creates a new {@link PutDocumentsRequest} object. */
         @NonNull
         public PutDocumentsRequest build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBuilt = true;
             return new PutDocumentsRequest(mDocuments);
         }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mDocuments = new ArrayList<>(mDocuments);
+                mBuilt = false;
+            }
+        }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
new file mode 100644
index 0000000..addb96a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Encapsulates a request to remove documents by namespace and IDs from the
+ * {@link AppSearchSession} database.
+ *
+ * @see AppSearchSession#remove
+ */
+public final class RemoveByDocumentIdRequest {
+    private final String mNamespace;
+    private final Set<String> mIds;
+
+    RemoveByDocumentIdRequest(String namespace, Set<String> ids) {
+        mNamespace = namespace;
+        mIds = ids;
+    }
+
+    /** Returns the namespace to remove documents from. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the set of document IDs attached to the request. */
+    @NonNull
+    public Set<String> getIds() {
+        return Collections.unmodifiableSet(mIds);
+    }
+
+    /** Builder for {@link RemoveByDocumentIdRequest} objects. */
+    public static final class Builder {
+        private final String mNamespace;
+        private ArraySet<String> mIds = new ArraySet<>();
+        private boolean mBuilt = false;
+
+        /** Creates a {@link RemoveByDocumentIdRequest.Builder} instance. */
+        public Builder(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+        }
+
+        /** Adds one or more document IDs to the request. */
+        @NonNull
+        public Builder addIds(@NonNull String... ids) {
+            Preconditions.checkNotNull(ids);
+            resetIfBuilt();
+            return addIds(Arrays.asList(ids));
+        }
+
+        /** Adds a collection of IDs to the request. */
+        @NonNull
+        public Builder addIds(@NonNull Collection<String> ids) {
+            Preconditions.checkNotNull(ids);
+            resetIfBuilt();
+            mIds.addAll(ids);
+            return this;
+        }
+
+        /** Builds a new {@link RemoveByDocumentIdRequest}. */
+        @NonNull
+        public RemoveByDocumentIdRequest build() {
+            mBuilt = true;
+            return new RemoveByDocumentIdRequest(mNamespace, mIds);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mIds = new ArraySet<>(mIds);
+                mBuilt = false;
+            }
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
index ed7cad9..680ad83 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
@@ -17,6 +17,7 @@
 package androidx.appsearch.app;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
@@ -26,17 +27,18 @@
 import java.util.Set;
 
 /**
- * Encapsulates a request to remove documents by namespace and URI.
- *
- * @see AppSearchSession#removeByUri
+ * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+ * @hide
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Deprecated
 public final class RemoveByUriRequest {
     private final String mNamespace;
-    private final Set<String> mUris;
+    private final Set<String> mIds;
 
-    RemoveByUriRequest(String namespace, Set<String> uris) {
+    RemoveByUriRequest(String namespace, Set<String> ids) {
         mNamespace = namespace;
-        mUris = uris;
+        mIds = ids;
     }
 
     /** Returns the namespace to remove documents from. */
@@ -45,53 +47,82 @@
         return mNamespace;
     }
 
-    /** Returns the URIs of documents to remove from the namespace. */
+    /** Returns the set of document IDs attached to the request. */
     @NonNull
     public Set<String> getUris() {
-        return Collections.unmodifiableSet(mUris);
+        return Collections.unmodifiableSet(mIds);
     }
 
-    /** Builder for {@link RemoveByUriRequest} objects. */
+    /**
+     * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Deprecated
+    @NonNull
+    public RemoveByDocumentIdRequest toRemoveByDocumentIdRequest() {
+        return new RemoveByDocumentIdRequest.Builder(mNamespace).addIds(mIds).build();
+    }
+
+    /**
+     * Builder for {@link RemoveByUriRequest} objects.
+     *
+     * <p>Once {@link #build} is called, the instance can no longer be used.
+     */
     public static final class Builder {
-        private String mNamespace = GenericDocument.DEFAULT_NAMESPACE;
-        private final Set<String> mUris = new ArraySet<>();
+        private final String mNamespace;
+        private final Set<String> mIds = new ArraySet<>();
         private boolean mBuilt = false;
 
         /**
-         * Sets which namespace these documents will be removed from.
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        public Builder(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+        }
+
+        /**
+         * Adds one or more document IDs to the request.
          *
-         * <p>If this is not set, it defaults to {@link GenericDocument#DEFAULT_NAMESPACE}.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
-        public Builder setNamespace(@NonNull String namespace) {
+        public Builder addUris(@NonNull String... ids) {
+            Preconditions.checkNotNull(ids);
+            return addUris(Arrays.asList(ids));
+        }
+
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        @NonNull
+        public Builder addUris(@NonNull Collection<String> ids) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(namespace);
-            mNamespace = namespace;
+            Preconditions.checkNotNull(ids);
+            mIds.addAll(ids);
             return this;
         }
 
-        /** Adds one or more URIs to the request. */
-        @NonNull
-        public Builder addUri(@NonNull String... uris) {
-            Preconditions.checkNotNull(uris);
-            return addUri(Arrays.asList(uris));
-        }
-
-        /** Adds one or more URIs to the request. */
-        @NonNull
-        public Builder addUri(@NonNull Collection<String> uris) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(uris);
-            mUris.addAll(uris);
-            return this;
-        }
-
-        /** Builds a new {@link RemoveByUriRequest}. */
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
         @NonNull
         public RemoveByUriRequest build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBuilt = true;
-            return new RemoveByUriRequest(mNamespace, mUris);
+            return new RemoveByUriRequest(mNamespace, mIds);
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
new file mode 100644
index 0000000..db26931
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+/**
+ * A request to report usage of a document owned by another app from a system UI surface.
+ *
+ * <p>Usage reported in this way is measured separately from usage reported via
+ * {@link AppSearchSession#reportUsage}.
+ *
+ * <p>See {@link GlobalSearchSession#reportSystemUsage} for a detailed description of usage
+ * reporting.
+ */
+public final class ReportSystemUsageRequest {
+    private final String mPackageName;
+    private final String mDatabase;
+    private final String mNamespace;
+    private final String mDocumentId;
+    private final long mUsageTimestampMillis;
+
+    ReportSystemUsageRequest(
+            @NonNull String packageName,
+            @NonNull String database,
+            @NonNull String namespace,
+            @NonNull String documentId,
+            long usageTimestampMillis) {
+        mPackageName = Preconditions.checkNotNull(packageName);
+        mDatabase = Preconditions.checkNotNull(database);
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mDocumentId = Preconditions.checkNotNull(documentId);
+        mUsageTimestampMillis = usageTimestampMillis;
+    }
+
+    /** Returns the package name of the app which owns the document that was used. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns the database in which the document that was used resides. */
+    @NonNull
+    public String getDatabaseName() {
+        return mDatabase;
+    }
+
+    /** Returns the namespace of the document that was used. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the ID of document that was used. */
+    @NonNull
+    public String getDocumentId() {
+        return mDocumentId;
+    }
+
+    /**
+     * Returns the timestamp in milliseconds of the usage report (the time at which the document
+     * was used).
+     *
+     * <p>The value is in the {@link System#currentTimeMillis} time base.
+     */
+    /*@exportToFramework:CurrentTimeMillisLong*/
+    public long getUsageTimestampMillis() {
+        return mUsageTimestampMillis;
+    }
+
+    /** Builder for {@link ReportSystemUsageRequest} objects. */
+    public static final class Builder {
+        private final String mPackageName;
+        private final String mDatabase;
+        private final String mNamespace;
+        private final String mDocumentId;
+        private Long mUsageTimestampMillis;
+
+        /**
+         * Creates a {@link ReportSystemUsageRequest.Builder} instance.
+         *
+         * @param packageName  The package name of the app which owns the document that was used
+         *                     (e.g. from {@link SearchResult#getPackageName}).
+         * @param databaseName The database in which the document that was used resides (e.g. from
+         *                     {@link SearchResult#getDatabaseName}).
+         * @param namespace    The namespace of the document that was used (e.g. from
+         *                     {@link GenericDocument#getNamespace}.
+         * @param documentId   The ID of document that was used (e.g. from
+         *                     {@link GenericDocument#getId}.
+         */
+        public Builder(
+                @NonNull String packageName,
+                @NonNull String databaseName,
+                @NonNull String namespace,
+                @NonNull String documentId) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            mDatabase = Preconditions.checkNotNull(databaseName);
+            mNamespace = Preconditions.checkNotNull(namespace);
+            mDocumentId = Preconditions.checkNotNull(documentId);
+        }
+
+        /**
+         * Sets the timestamp in milliseconds of the usage report (the time at which the document
+         * was used).
+         *
+         * <p>The value is in the {@link System#currentTimeMillis} time base.
+         *
+         * <p>If unset, this defaults to the current timestamp at the time that the
+         * {@link ReportSystemUsageRequest} is constructed.
+         */
+        @NonNull
+        public ReportSystemUsageRequest.Builder setUsageTimestampMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
+            mUsageTimestampMillis = usageTimestampMillis;
+            return this;
+        }
+
+        /** Builds a new {@link ReportSystemUsageRequest}. */
+        @NonNull
+        public ReportSystemUsageRequest build() {
+            if (mUsageTimestampMillis == null) {
+                mUsageTimestampMillis = System.currentTimeMillis();
+            }
+            return new ReportSystemUsageRequest(
+                    mPackageName, mDatabase, mNamespace, mDocumentId, mUsageTimestampMillis);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
new file mode 100644
index 0000000..53d8870
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * A request to report usage of a document.
+ *
+ * <p>See {@link AppSearchSession#reportUsage} for a detailed description of usage reporting.
+ *
+ * @see AppSearchSession#reportUsage
+ */
+public final class ReportUsageRequest {
+    private final String mNamespace;
+    private final String mDocumentId;
+    private final long mUsageTimestampMillis;
+
+    ReportUsageRequest(
+            @NonNull String namespace, @NonNull String documentId, long usageTimestampMillis) {
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mDocumentId = Preconditions.checkNotNull(documentId);
+        mUsageTimestampMillis = usageTimestampMillis;
+    }
+
+    /** Returns the namespace of the document that was used. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the ID of document that was used. */
+    @NonNull
+    public String getDocumentId() {
+        return mDocumentId;
+    }
+
+    /**
+     * Returns the timestamp in milliseconds of the usage report (the time at which the document
+     * was used).
+     *
+     * <p>The value is in the {@link System#currentTimeMillis} time base.
+     */
+    /*@exportToFramework:CurrentTimeMillisLong*/
+    public long getUsageTimestampMillis() {
+        return mUsageTimestampMillis;
+    }
+
+    /** Builder for {@link ReportUsageRequest} objects. */
+    public static final class Builder {
+        private final String mNamespace;
+        // TODO(b/181887768): Make this final
+        private String mDocumentId;
+        private Long mUsageTimestampMillis;
+
+        /**
+         * Creates a new {@link ReportUsageRequest.Builder} instance.
+         *
+         * @param namespace    The namespace of the document that was used (e.g. from
+         *                     {@link GenericDocument#getNamespace}.
+         * @param documentId   The ID of document that was used (e.g. from
+         *                     {@link GenericDocument#getId}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String documentId) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+            mDocumentId = Preconditions.checkNotNull(documentId);
+        }
+
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        public Builder(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+        }
+
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        @NonNull
+        public Builder setUri(@NonNull String uri) {
+            mDocumentId = uri;
+            return this;
+        }
+
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        @NonNull
+        public ReportUsageRequest.Builder setUsageTimeMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
+            return setUsageTimestampMillis(usageTimestampMillis);
+        }
+
+        /**
+         * Sets the timestamp in milliseconds of the usage report (the time at which the document
+         * was used).
+         *
+         * <p>The value is in the {@link System#currentTimeMillis} time base.
+         *
+         * <p>If unset, this defaults to the current timestamp at the time that the
+         * {@link ReportUsageRequest} is constructed.
+         */
+        @NonNull
+        public ReportUsageRequest.Builder setUsageTimestampMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
+            mUsageTimestampMillis = usageTimestampMillis;
+            return this;
+        }
+
+        /** Builds a new {@link ReportUsageRequest}. */
+        @NonNull
+        public ReportUsageRequest build() {
+            if (mUsageTimestampMillis == null) {
+                mUsageTimestampMillis = System.currentTimeMillis();
+            }
+            return new ReportUsageRequest(mNamespace, mDocumentId, mUsageTimestampMillis);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
index dcddb5e..b4b5f56 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -21,6 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.exceptions.AppSearchException;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
@@ -32,9 +33,9 @@
  *
  * <p>This allows clients to obtain:
  * <ul>
- *   <li>The document which matched, using {@link #getDocument}
+ *   <li>The document which matched, using {@link #getGenericDocument}
  *   <li>Information about which properties in the document matched, and "snippet" information
- *       containing textual summaries of the document's matches, using {@link #getMatches}
+ *       containing textual summaries of the document's matches, using {@link #getMatchInfos}
  *  </ul>
  *
  * <p>"Snippet" refers to a substring of text from the content of document that is returned as a
@@ -43,17 +44,11 @@
  * @see SearchResults
  */
 public final class SearchResult {
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static final String DOCUMENT_FIELD = "document";
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static final String MATCHES_FIELD = "matches";
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static final String PACKAGE_NAME_FIELD = "packageName";
+    static final String DOCUMENT_FIELD = "document";
+    static final String MATCH_INFOS_FIELD = "matchInfos";
+    static final String PACKAGE_NAME_FIELD = "packageName";
+    static final String DATABASE_NAME_FIELD = "databaseName";
+    static final String RANKING_SIGNAL_FIELD = "rankingSignal";
 
     @NonNull
     private final Bundle mBundle;
@@ -64,7 +59,7 @@
 
     /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */
     @Nullable
-    private List<MatchInfo> mMatches;
+    private List<MatchInfo> mMatchInfos;
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -79,13 +74,30 @@
         return mBundle;
     }
 
+// @exportToFramework:startStrip()
+    /**
+     * Contains the matching document, converted to the given document class.
+     *
+     * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class)}.
+     *
+     * @return Document object which matched the query.
+     * @throws AppSearchException if no factory for this document class could be found on the
+     *       classpath.
+     */
+    @NonNull
+    public <T> T getDocument(@NonNull Class<T> documentClass) throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        return getGenericDocument().toDocumentClass(documentClass);
+    }
+// @exportToFramework:endStrip()
+
     /**
      * Contains the matching {@link GenericDocument}.
      *
      * @return Document object which matched the query.
      */
     @NonNull
-    public GenericDocument getDocument() {
+    public GenericDocument getGenericDocument() {
         if (mDocument == null) {
             mDocument = new GenericDocument(
                     Preconditions.checkNotNull(mBundle.getBundle(DOCUMENT_FIELD)));
@@ -94,7 +106,20 @@
     }
 
     /**
-     * Contains a list of Snippets that matched the request.
+     * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Deprecated
+    /*@exportToFramework:UnsupportedAppUsage*/
+    @NonNull
+    public List<MatchInfo> getMatches() {
+        return getMatchInfos();
+    }
+
+    /**
+     * Returns a list of {@link MatchInfo}s providing information about how the document in
+     * {@link #getGenericDocument} matched the query.
      *
      * @return List of matches based on {@link SearchSpec}. If snippeting is disabled using
      * {@link SearchSpec.Builder#setSnippetCount} or
@@ -102,17 +127,17 @@
      * value, this method returns an empty list.
      */
     @NonNull
-    public List<MatchInfo> getMatches() {
-        if (mMatches == null) {
+    public List<MatchInfo> getMatchInfos() {
+        if (mMatchInfos == null) {
             List<Bundle> matchBundles =
-                    Preconditions.checkNotNull(mBundle.getParcelableArrayList(MATCHES_FIELD));
-            mMatches = new ArrayList<>(matchBundles.size());
+                    Preconditions.checkNotNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD));
+            mMatchInfos = new ArrayList<>(matchBundles.size());
             for (int i = 0; i < matchBundles.size(); i++) {
-                MatchInfo matchInfo = new MatchInfo(getDocument(), matchBundles.get(i));
-                mMatches.add(matchInfo);
+                MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument());
+                mMatchInfos.add(matchInfo);
             }
         }
-        return mMatches;
+        return mMatchInfos;
     }
 
     /**
@@ -126,6 +151,145 @@
     }
 
     /**
+     * Contains the database name that stored the {@link GenericDocument}.
+     *
+     * @return Name of the database within which the document is stored
+     */
+    @NonNull
+    public String getDatabaseName() {
+        return Preconditions.checkNotNull(mBundle.getString(DATABASE_NAME_FIELD));
+    }
+
+    /**
+     * Returns the ranking signal of the {@link GenericDocument}, according to the
+     * ranking strategy set in {@link SearchSpec.Builder#setRankingStrategy(int)}.
+     *
+     * The meaning of the ranking signal and its value is determined by the selected ranking
+     * strategy:
+     * <ul>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_NONE} - this value will be 0</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling
+     * {@link GenericDocument#getScore()} on the document returned by
+     * {@link #getGenericDocument()}</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling
+     * {@link GenericDocument#getCreationTimestampMillis()} on the document returned by
+     * {@link #getGenericDocument()}</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where
+     * a higher value means more relevant</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been
+     * reported for the document returned by {@link #getGenericDocument()}</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} - the timestamp of the
+     * most recent usage that has been reported for the document returned by
+     * {@link #getGenericDocument()}</li>
+     * </ul>
+     *
+     * @return Ranking signal of the document
+     */
+    public double getRankingSignal() {
+        return mBundle.getDouble(RANKING_SIGNAL_FIELD);
+    }
+
+    /** Builder for {@link SearchResult} objects. */
+    public static final class Builder {
+        private final String mPackageName;
+        private final String mDatabaseName;
+        private ArrayList<Bundle> mMatchInfoBundles = new ArrayList<>();
+        private GenericDocument mGenericDocument;
+        private double mRankingSignal;
+        private boolean mBuilt = false;
+
+        /**
+         * Constructs a new builder for {@link SearchResult} objects.
+         *
+         * @param packageName the package name the matched document belongs to
+         * @param databaseName the database name the matched document belongs to.
+         */
+        public Builder(@NonNull String packageName, @NonNull String databaseName) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            mDatabaseName = Preconditions.checkNotNull(databaseName);
+        }
+
+// @exportToFramework:startStrip()
+        /**
+         * Sets the document which matched.
+         *
+         * @param document An instance of a class annotated with
+         * {@link androidx.appsearch.annotation.Document}.
+         *
+         * @throws AppSearchException if an error occurs converting a document class into a
+         *                            {@link GenericDocument}.
+         */
+        @NonNull
+        public Builder setDocument(@NonNull Object document) throws AppSearchException {
+            Preconditions.checkNotNull(document);
+            resetIfBuilt();
+            return setGenericDocument(GenericDocument.fromDocumentClass(document));
+        }
+// @exportToFramework:endStrip()
+
+        /** Sets the document which matched. */
+        @NonNull
+        public Builder setGenericDocument(@NonNull GenericDocument document) {
+            Preconditions.checkNotNull(document);
+            resetIfBuilt();
+            mGenericDocument = document;
+            return this;
+        }
+
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        @NonNull
+        public Builder addMatch(@NonNull MatchInfo matchInfo) {
+            return addMatchInfo(matchInfo);
+        }
+
+        /** Adds another match to this SearchResult. */
+        @NonNull
+        public Builder addMatchInfo(@NonNull MatchInfo matchInfo) {
+            Preconditions.checkState(
+                    matchInfo.mDocument == null,
+                    "This MatchInfo is already associated with a SearchResult and can't be "
+                            + "reassigned");
+            resetIfBuilt();
+            mMatchInfoBundles.add(matchInfo.mBundle);
+            return this;
+        }
+
+        /** Sets the ranking signal of the matched document in this SearchResult. */
+        @NonNull
+        public Builder setRankingSignal(double rankingSignal) {
+            resetIfBuilt();
+            mRankingSignal = rankingSignal;
+            return this;
+        }
+
+        /** Constructs a new {@link SearchResult}. */
+        @NonNull
+        public SearchResult build() {
+            Bundle bundle = new Bundle();
+            bundle.putString(PACKAGE_NAME_FIELD, mPackageName);
+            bundle.putString(DATABASE_NAME_FIELD, mDatabaseName);
+            bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle());
+            bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal);
+            bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles);
+            mBuilt = true;
+            return new SearchResult(bundle);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles);
+                mBuilt = false;
+            }
+        }
+    }
+
+    /**
      * 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.
@@ -139,9 +303,9 @@
      * <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#getExactMatchRange()} returns [29, 32]
      * <p>{@link MatchInfo#getExactMatch()} returns "foo"
-     * <p>{@link MatchInfo#getSnippetPosition()} returns [26, 33]
+     * <p>{@link MatchInfo#getSnippetRange()} returns [26, 33]
      * <p>{@link MatchInfo#getSnippet()} returns "is foo."
      * <p>
      * <p>Class Example 2:
@@ -155,70 +319,62 @@
      * <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#getExactMatchRange()} returns [0, 4]
      * <p>{@link MatchInfo#getExactMatch()} returns "Test"
-     * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
+     * <p>{@link MatchInfo#getSnippetRange()} 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#getExactMatchRange()} returns [0, 20]
      * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
-     * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
+     * <p>{@link MatchInfo#getSnippetRange()} returns [0, 20]
      * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
      */
     public static final class MatchInfo {
-        /**
-         * The path of the matching snippet property.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String PROPERTY_PATH_FIELD = "propertyPath";
+        /** The path of the matching snippet property. */
+        private static final String PROPERTY_PATH_FIELD = "propertyPath";
+        private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower";
+        private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper";
+        private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower";
+        private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper";
 
-        /**
-         * The index of matching value in its property. A property may have multiple values. This
-         * index indicates which value is the match.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String VALUES_INDEX_FIELD = "valuesIndex";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String EXACT_MATCH_POSITION_LOWER_FIELD = "exactMatchPositionLower";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String EXACT_MATCH_POSITION_UPPER_FIELD = "exactMatchPositionUpper";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String WINDOW_POSITION_LOWER_FIELD = "windowPositionLower";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String WINDOW_POSITION_UPPER_FIELD = "windowPositionUpper";
-
-        private final String mFullText;
         private final String mPropertyPath;
-        private final Bundle mBundle;
+        final Bundle mBundle;
+
+        /**
+         * Document which the match comes from.
+         *
+         * <p>If this is {@code null}, methods which require access to the document, like
+         * {@link #getExactMatch}, will throw {@link NullPointerException}.
+         */
+        @Nullable
+        final GenericDocument mDocument;
+
+        /** Full text of the matched property. Populated on first use. */
+        @Nullable
+        private String mFullText;
+
+        /** Range of property that exactly matched the query. Populated on first use. */
+        @Nullable
         private MatchRange mExactMatchRange;
+
+        /** Range of some reasonable amount of context around the query. Populated on first use. */
+        @Nullable
         private MatchRange mWindowRange;
 
-        MatchInfo(@NonNull GenericDocument document, @NonNull Bundle bundle) {
+        MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) {
             mBundle = Preconditions.checkNotNull(bundle);
-            Preconditions.checkNotNull(document);
+            mDocument = 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>A property path is a '.' - 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"
          */
@@ -234,6 +390,12 @@
          */
         @NonNull
         public String getFullText() {
+            if (mFullText == null) {
+                Preconditions.checkState(
+                        mDocument != null,
+                        "Document has not been populated; this MatchInfo cannot be used yet");
+                mFullText = getPropertyValues(mDocument, mPropertyPath);
+            }
             return mFullText;
         }
 
@@ -242,11 +404,11 @@
          * <p>For class example 1 this returns [29, 32]
          */
         @NonNull
-        public MatchRange getExactMatchPosition() {
+        public MatchRange getExactMatchRange() {
             if (mExactMatchRange == null) {
                 mExactMatchRange = new MatchRange(
-                        mBundle.getInt(EXACT_MATCH_POSITION_LOWER_FIELD),
-                        mBundle.getInt(EXACT_MATCH_POSITION_UPPER_FIELD));
+                        mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD),
+                        mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD));
             }
             return mExactMatchRange;
         }
@@ -257,7 +419,7 @@
          */
         @NonNull
         public CharSequence getExactMatch() {
-            return getSubstring(getExactMatchPosition());
+            return getSubstring(getExactMatchRange());
         }
 
         /**
@@ -267,11 +429,11 @@
          * <p>For class example 1 this returns [29, 41].
          */
         @NonNull
-        public MatchRange getSnippetPosition() {
+        public MatchRange getSnippetRange() {
             if (mWindowRange == null) {
                 mWindowRange = new MatchRange(
-                        mBundle.getInt(WINDOW_POSITION_LOWER_FIELD),
-                        mBundle.getInt(WINDOW_POSITION_UPPER_FIELD));
+                        mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD),
+                        mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD));
             }
             return mWindowRange;
         }
@@ -286,7 +448,7 @@
          */
         @NonNull
         public CharSequence getSnippet() {
-            return getSubstring(getSnippetPosition());
+            return getSubstring(getSnippetRange());
         }
 
         private CharSequence getSubstring(MatchRange range) {
@@ -294,18 +456,66 @@
         }
 
         /** Extracts the matching string from the document. */
-        private static String getPropertyValues(
-                GenericDocument document, String propertyName, int valueIndex) {
+        private static String getPropertyValues(GenericDocument document, String propertyName) {
             // 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!");
+            String result = document.getPropertyString(propertyName);
+            if (result == null) {
+                throw new IllegalStateException(
+                        "No content found for requested property path: " + propertyName);
             }
-            return values[valueIndex];
+            return result;
+        }
+
+        /** Builder for {@link MatchInfo} objects. */
+        public static final class Builder {
+            private final String mPropertyPath;
+            private MatchRange mExactMatchRange = new MatchRange(0, 0);
+            private MatchRange mSnippetRange = new MatchRange(0, 0);
+
+            /**
+             * Creates a new {@link MatchInfo.Builder} reporting a match with the given property
+             * path.
+             *
+             * <p>A property path is a dot-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".
+             *
+             * @param propertyPath A {@code dot-delimited sequence of property names indicating
+             *                     which property in the document these snippets correspond to.
+             */
+            public Builder(@NonNull String propertyPath) {
+                mPropertyPath = Preconditions.checkNotNull(propertyPath);
+            }
+
+            /** Sets the exact {@link MatchRange} corresponding to the given entry. */
+            @NonNull
+            public Builder setExactMatchRange(@NonNull MatchRange matchRange) {
+                mExactMatchRange = Preconditions.checkNotNull(matchRange);
+                return this;
+            }
+
+            /** Sets the snippet {@link MatchRange} corresponding to the given entry. */
+            @NonNull
+            public Builder setSnippetRange(@NonNull MatchRange matchRange) {
+                mSnippetRange = Preconditions.checkNotNull(matchRange);
+                return this;
+            }
+
+            /** Constructs a new {@link MatchInfo}. */
+            @NonNull
+            public MatchInfo build() {
+                Bundle bundle = new Bundle();
+                bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath);
+                bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart());
+                bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd());
+                bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart());
+                bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd());
+                return new MatchInfo(bundle, /*document=*/ null);
+            }
         }
     }
 
@@ -328,9 +538,7 @@
          *
          * @param start The start point (inclusive)
          * @param end   The end point (exclusive)
-         * @hide
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public MatchRange(int start, int end) {
             if (start > end) {
                 throw new IllegalArgumentException("Start point must be less than or equal to "
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
index cdd3f3e..6bf301f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
@@ -24,25 +24,30 @@
 import java.util.List;
 
 /**
- * SearchResults are a returned object from a query API.
+ * Encapsulates results of a search operation.
  *
- * <p>Each {@link SearchResult} contains a document and may contain other fields like snippets
- * based on request.
+ * <p>Each {@link AppSearchSession#search} operation returns a list of {@link SearchResult}
+ * objects, referred to as a "page", limited by the size configured by
+ * {@link SearchSpec.Builder#setResultCountPerPage}.
  *
- * <p>Should close this object after finish fetching results.
+ * <p>To fetch a page of results, call {@link #getNextPage()}.
+ *
+ * <p>All instances of {@link SearchResults} must call {@link SearchResults#close()} after the
+ * results are fetched.
  *
  * <p>This class is not thread safe.
  */
 public interface SearchResults extends Closeable {
     /**
-     * Gets a whole page of {@link SearchResult}s.
+     * Retrieves the next page of {@link SearchResult} objects.
      *
-     * <p>Re-call this method to get next page of {@link SearchResult}, until it returns an
-     * empty list.
+     * <p>The page size is configured by {@link SearchSpec.Builder#setResultCountPerPage}.
      *
-     * <p>The page size is set by {@link SearchSpec.Builder#setResultCountPerPage}.
+     * <p>Continue calling this method to access results until it returns an empty list,
+     * signifying there are no more results.
      *
-     * @return The pending result of performing this operation.
+     * @return a {@link ListenableFuture} which resolves to a list of {@link SearchResult}
+     * objects.
      */
     @NonNull
     ListenableFuture<List<SearchResult>> getNextPage();
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 5c939dd..756cddf 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -23,8 +23,9 @@
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.Document;
 import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.exceptions.IllegalSearchSpecException;
+import androidx.appsearch.util.BundleUtil;
 import androidx.collection.ArrayMap;
 import androidx.core.util.Preconditions;
 
@@ -52,8 +53,9 @@
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
 
     static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
-    static final String SCHEMA_TYPE_FIELD = "schemaType";
+    static final String SCHEMA_FIELD = "schema";
     static final String NAMESPACE_FIELD = "namespace";
+    static final String PACKAGE_NAME_FIELD = "packageName";
     static final String NUM_PER_PAGE_FIELD = "numPerPage";
     static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
     static final String ORDER_FIELD = "order";
@@ -61,6 +63,8 @@
     static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
     static final String MAX_SNIPPET_FIELD = "maxSnippet";
     static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
+    static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
+    static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -75,6 +79,7 @@
 
     /**
      * Term Match Type for the query.
+     *
      * @hide
      */
     // NOTE: The integer values of these constants must match the proto enum constants in
@@ -84,7 +89,8 @@
             TERM_MATCH_PREFIX
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface TermMatch {}
+    public @interface TermMatch {
+    }
 
     /**
      * Query terms will only match exact tokens in the index.
@@ -99,6 +105,7 @@
 
     /**
      * Ranking Strategy for query result.
+     *
      * @hide
      */
     // NOTE: The integer values of these constants must match the proto enum constants in
@@ -107,12 +114,17 @@
             RANKING_STRATEGY_NONE,
             RANKING_STRATEGY_DOCUMENT_SCORE,
             RANKING_STRATEGY_CREATION_TIMESTAMP,
-            RANKING_STRATEGY_RELEVANCE_SCORE
+            RANKING_STRATEGY_RELEVANCE_SCORE,
+            RANKING_STRATEGY_USAGE_COUNT,
+            RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP,
+            RANKING_STRATEGY_SYSTEM_USAGE_COUNT,
+            RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface RankingStrategy {}
+    public @interface RankingStrategy {
+    }
 
-    /** No Ranking, results are returned in arbitrary order.*/
+    /** No Ranking, results are returned in arbitrary order. */
     public static final int RANKING_STRATEGY_NONE = 0;
     /** Ranked by app-provided document scores. */
     public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
@@ -120,9 +132,18 @@
     public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
     /** Ranked by document relevance score. */
     public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3;
+    /** Ranked by number of usages, as reported by the app. */
+    public static final int RANKING_STRATEGY_USAGE_COUNT = 4;
+    /** Ranked by timestamp of last usage, as reported by the app. */
+    public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5;
+    /** Ranked by number of usages from a system UI surface. */
+    public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6;
+    /** Ranked by timestamp of last usage from a system UI surface. */
+    public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7;
 
     /**
      * Order for query result.
+     *
      * @hide
      */
     // NOTE: The integer values of these constants must match the proto enum constants in
@@ -132,13 +153,39 @@
             ORDER_ASCENDING
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface Order {}
+    public @interface Order {
+    }
 
     /** Search results will be returned in a descending order. */
     public static final int ORDER_DESCENDING = 0;
     /** Search results will be returned in an ascending order. */
     public static final int ORDER_ASCENDING = 1;
 
+    /**
+     * Grouping type for result limits.
+     *
+     * @hide
+     */
+    @IntDef(flag = true, value = {
+            GROUPING_TYPE_PER_PACKAGE,
+            GROUPING_TYPE_PER_NAMESPACE
+    })
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GroupingType {
+    }
+
+    /**
+     * Results should be grouped together by package for the purpose of enforcing a limit on the
+     * number of results returned per package.
+     */
+    public static final int GROUPING_TYPE_PER_PACKAGE = 0b01;
+    /**
+     * Results should be grouped together by namespace for the purpose of enforcing a limit on the
+     * number of results returned per namespace.
+     */
+    public static final int GROUPING_TYPE_PER_NAMESPACE = 0b10;
+
     private final Bundle mBundle;
 
     /** @hide */
@@ -150,6 +197,7 @@
 
     /**
      * Returns the {@link Bundle} populated by this builder.
+     *
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -169,21 +217,21 @@
      * <p>If empty, the query will search over all schema types.
      */
     @NonNull
-    public List<String> getSchemaTypes() {
-        List<String> schemaTypes = mBundle.getStringArrayList(SCHEMA_TYPE_FIELD);
-        if (schemaTypes == null) {
+    public List<String> getFilterSchemas() {
+        List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
+        if (schemas == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(schemaTypes);
+        return Collections.unmodifiableList(schemas);
     }
 
     /**
-     * Returns the list of namespaces to search for.
+     * Returns the list of namespaces to search over.
      *
      * <p>If empty, the query will search over all namespaces.
      */
     @NonNull
-    public List<String> getNamespaces() {
+    public List<String> getFilterNamespaces() {
         List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
         if (namespaces == null) {
             return Collections.emptyList();
@@ -191,6 +239,22 @@
         return Collections.unmodifiableList(namespaces);
     }
 
+    /**
+     * Returns the list of package name filters to search over.
+     *
+     * <p>If empty, the query will search over all packages that the caller has access to. If
+     * package names are specified which caller doesn't have access to, then those package names
+     * will be ignored.
+     */
+    @NonNull
+    public List<String> getFilterPackageNames() {
+        List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);
+        if (packageNames == null) {
+            return Collections.emptyList();
+        }
+        return Collections.unmodifiableList(packageNames);
+    }
+
     /** Returns the number of results per page in the result set. */
     public int getResultCountPerPage() {
         return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
@@ -233,41 +297,63 @@
      */
     @NonNull
     public Map<String, List<String>> getProjections() {
-        Bundle typePropertyPathsBundle =
-                mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
-        Set<String> schemaTypes = typePropertyPathsBundle.keySet();
-        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemaTypes.size());
-        for (String schemaType : schemaTypes) {
-            typePropertyPathsMap.put(schemaType,
-                    typePropertyPathsBundle.getStringArrayList(schemaType));
+        Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
+        Set<String> schemas = typePropertyPathsBundle.keySet();
+        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            typePropertyPathsMap.put(schema, typePropertyPathsBundle.getStringArrayList(schema));
         }
         return typePropertyPathsMap;
     }
 
+    /**
+     * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not
+     * called.
+     */
+    public @GroupingType int getResultGroupingTypeFlags() {
+        return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
+    }
+
+    /**
+     * Get the maximum number of results to return for each group.
+     *
+     * @return the maximum number of results to return for each group or Integer.MAX_VALUE if
+     * {@link Builder#setResultGrouping(int, int)} was not called.
+     */
+    public int getResultGroupingLimit() {
+        return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);
+    }
+
     /** Builder for {@link SearchSpec objects}. */
     public static final class Builder {
+        private ArrayList<String> mSchemas = new ArrayList<>();
+        private ArrayList<String> mNamespaces = new ArrayList<>();
+        private ArrayList<String> mPackageNames = new ArrayList<>();
+        private Bundle mProjectionTypePropertyMasks = new Bundle();
 
-        private final Bundle mBundle;
-        private final ArrayList<String> mSchemaTypes = new ArrayList<>();
-        private final ArrayList<String> mNamespaces = new ArrayList<>();
-        private final Bundle mProjectionTypePropertyMasks = new Bundle();
+        private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
+        private @TermMatch int mTermMatchType = TERM_MATCH_PREFIX;
+        private int mSnippetCount = 0;
+        private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
+        private int mMaxSnippetSize = 0;
+        private @RankingStrategy int mRankingStrategy = RANKING_STRATEGY_NONE;
+        private @Order int mOrder = ORDER_DESCENDING;
+        private @GroupingType int mGroupingTypeFlags = 0;
+        private int mGroupingLimit = 0;
         private boolean mBuilt = false;
 
-        /** Creates a new {@link SearchSpec.Builder}. */
-        public Builder() {
-            mBundle = new Bundle();
-            mBundle.putInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
-        }
-
         /**
          * Indicates how the query terms should match {@code TermMatchCode} in the index.
+         *
+         * <p>If this method is not called, the default term match type is
+         * {@link SearchSpec#TERM_MATCH_PREFIX}.
          */
         @NonNull
-        public Builder setTermMatch(@TermMatch int termMatchTypeCode) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkArgumentInRange(termMatchTypeCode, TERM_MATCH_EXACT_ONLY,
+        public Builder setTermMatch(@TermMatch int termMatchType) {
+            Preconditions.checkArgumentInRange(termMatchType, TERM_MATCH_EXACT_ONLY,
                     TERM_MATCH_PREFIX, "Term match type");
-            mBundle.putInt(TERM_MATCH_TYPE_FIELD, termMatchTypeCode);
+            resetIfBuilt();
+            mTermMatchType = termMatchType;
             return this;
         }
 
@@ -278,10 +364,10 @@
          * <p>If unset, the query will search over all schema types.
          */
         @NonNull
-        public Builder addSchemaType(@NonNull String... schemaTypes) {
-            Preconditions.checkNotNull(schemaTypes);
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            return addSchemaType(Arrays.asList(schemaTypes));
+        public Builder addFilterSchemas(@NonNull String... schemas) {
+            Preconditions.checkNotNull(schemas);
+            resetIfBuilt();
+            return addFilterSchemas(Arrays.asList(schemas));
         }
 
         /**
@@ -291,56 +377,59 @@
          * <p>If unset, the query will search over all schema types.
          */
         @NonNull
-        public Builder addSchemaType(@NonNull Collection<String> schemaTypes) {
-            Preconditions.checkNotNull(schemaTypes);
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mSchemaTypes.addAll(schemaTypes);
+        public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
+            Preconditions.checkNotNull(schemas);
+            resetIfBuilt();
+            mSchemas.addAll(schemas);
             return this;
         }
 
 // @exportToFramework:startStrip()
+
         /**
-         * Adds the Schema type of given data classes to the Schema type filter of
+         * Adds the Schema names of given document classes to the Schema type filter of
          * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
          *
          * <p>If unset, the query will search over all schema types.
          *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with {@link Document}.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemaTypes
+        // Merged list available from getFilterSchemas
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addSchemaByDataClass(@NonNull Collection<? extends Class<?>> dataClasses)
-                throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            List<String> schemaTypes = new ArrayList<>(dataClasses.size());
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            for (Class<?> dataClass : dataClasses) {
-                DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
-                schemaTypes.add(factory.getSchemaType());
+        public Builder addFilterDocumentClasses(
+                @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+            Preconditions.checkNotNull(documentClasses);
+            resetIfBuilt();
+            List<String> schemas = new ArrayList<>(documentClasses.size());
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            for (Class<?> documentClass : documentClasses) {
+                DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+                schemas.add(factory.getSchemaName());
             }
-            addSchemaType(schemaTypes);
+            addFilterSchemas(schemas);
             return this;
         }
 // @exportToFramework:endStrip()
 
 // @exportToFramework:startStrip()
+
         /**
-         * Adds the Schema type of given data classes to the Schema type filter of
+         * Adds the Schema names of given document classes to the Schema type filter of
          * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
          *
          * <p>If unset, the query will search over all schema types.
          *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with {@link Document}.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemas()
+        // Merged list available from getFilterSchemas()
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addSchemaByDataClass(@NonNull Class<?>... dataClasses)
+        public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
                 throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            return addSchemaByDataClass(Arrays.asList(dataClasses));
+            Preconditions.checkNotNull(documentClasses);
+            resetIfBuilt();
+            return addFilterDocumentClasses(Arrays.asList(documentClasses));
         }
 // @exportToFramework:endStrip()
 
@@ -350,10 +439,10 @@
          * <p>If unset, the query will search over all namespaces.
          */
         @NonNull
-        public Builder addNamespace(@NonNull String... namespaces) {
+        public Builder addFilterNamespaces(@NonNull String... namespaces) {
             Preconditions.checkNotNull(namespaces);
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            return addNamespace(Arrays.asList(namespaces));
+            resetIfBuilt();
+            return addFilterNamespaces(Arrays.asList(namespaces));
         }
 
         /**
@@ -362,34 +451,66 @@
          * <p>If unset, the query will search over all namespaces.
          */
         @NonNull
-        public Builder addNamespace(@NonNull Collection<String> namespaces) {
+        public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
             Preconditions.checkNotNull(namespaces);
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            resetIfBuilt();
             mNamespaces.addAll(namespaces);
             return this;
         }
 
         /**
+         * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
+         * were indexed from the specified packages.
+         *
+         * <p>If unset, the query will search over all packages that the caller has access to.
+         * If package names are specified which caller doesn't have access to, then those package
+         * names will be ignored.
+         */
+        @NonNull
+        public Builder addFilterPackageNames(@NonNull String... packageNames) {
+            Preconditions.checkNotNull(packageNames);
+            resetIfBuilt();
+            return addFilterPackageNames(Arrays.asList(packageNames));
+        }
+
+        /**
+         * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
+         * were indexed from the specified packages.
+         *
+         * <p>If unset, the query will search over all packages that the caller has access to.
+         * If package names are specified which caller doesn't have access to, then those package
+         * names will be ignored.
+         */
+        @NonNull
+        public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) {
+            Preconditions.checkNotNull(packageNames);
+            resetIfBuilt();
+            mPackageNames.addAll(packageNames);
+            return this;
+        }
+
+        /**
          * Sets the number of results per page in the returned object.
          *
          * <p>The default number of results per page is 10.
          */
         @NonNull
         public SearchSpec.Builder setResultCountPerPage(
-                @IntRange(from = 0, to = MAX_NUM_PER_PAGE) int numPerPage) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkArgumentInRange(numPerPage, 0, MAX_NUM_PER_PAGE, "NumPerPage");
-            mBundle.putInt(NUM_PER_PAGE_FIELD, numPerPage);
+                @IntRange(from = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage) {
+            Preconditions.checkArgumentInRange(
+                    resultCountPerPage, 0, MAX_NUM_PER_PAGE, "resultCountPerPage");
+            resetIfBuilt();
+            mResultCountPerPage = resultCountPerPage;
             return this;
         }
 
-        /** Sets ranking strategy for AppSearch results.*/
+        /** Sets ranking strategy for AppSearch results. */
         @NonNull
         public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkArgumentInRange(rankingStrategy, RANKING_STRATEGY_NONE,
-                    RANKING_STRATEGY_RELEVANCE_SCORE, "Result ranking strategy");
-            mBundle.putInt(RANKING_STRATEGY_FIELD, rankingStrategy);
+                    RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP, "Result ranking strategy");
+            resetIfBuilt();
+            mRankingStrategy = rankingStrategy;
             return this;
         }
 
@@ -401,10 +522,10 @@
          */
         @NonNull
         public Builder setOrder(@Order int order) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkArgumentInRange(order, ORDER_DESCENDING, ORDER_ASCENDING,
                     "Result ranking order");
-            mBundle.putInt(ORDER_FIELD, order);
+            resetIfBuilt();
+            mOrder = order;
             return this;
         }
 
@@ -412,33 +533,40 @@
          * 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 SearchResult#getMatches} will
-         * return {@code null} for that result.
+         * <p>The list returned from {@link SearchResult#getMatchInfos} will contain at most this
+         * many entries.
+         *
+         * <p>If set to 0 (default), snippeting is disabled and the list returned from
+         * {@link SearchResult#getMatchInfos} will be empty.
          */
         @NonNull
         public SearchSpec.Builder setSnippetCount(
                 @IntRange(from = 0, to = MAX_SNIPPET_COUNT) int snippetCount) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount");
-            mBundle.putInt(SNIPPET_COUNT_FIELD, snippetCount);
+            resetIfBuilt();
+            mSnippetCount = snippetCount;
             return this;
         }
 
         /**
          * Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty}
-         * snippets for each property of {@link GenericDocument} will contain snippet information.
+         * snippets for each property of each {@link GenericDocument} will contain snippet
+         * information.
          *
-         * <p>If set to 0, snippeting is disabled and {@link SearchResult#getMatches}
-         * will return {@code null} for that result.
+         * <p>If set to 0, snippeting is disabled and the list
+         * returned from {@link SearchResult#getMatchInfos} will be empty.
+         *
+         * <p>The default behavior is to snippet all matches a property contains, up to the maximum
+         * value of 10,000.
          */
         @NonNull
         public SearchSpec.Builder setSnippetCountPerProperty(
                 @IntRange(from = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT)
                         int snippetCountPerProperty) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkArgumentInRange(snippetCountPerProperty,
                     0, MAX_SNIPPET_PER_PROPERTY_COUNT, "snippetCountPerProperty");
-            mBundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, snippetCountPerProperty);
+            resetIfBuilt();
+            mSnippetCountPerProperty = snippetCountPerProperty;
             return this;
         }
 
@@ -457,14 +585,14 @@
         @NonNull
         public SearchSpec.Builder setMaxSnippetSize(
                 @IntRange(from = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkArgumentInRange(
                     maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize");
-            mBundle.putInt(MAX_SNIPPET_FIELD, maxSnippetSize);
+            resetIfBuilt();
+            mMaxSnippetSize = maxSnippetSize;
             return this;
         }
 
-       /**
+        /**
          * Adds property paths for the specified type to be used for projection. If property
          * paths are added for a type, then only the properties referred to will be retrieved for
          * results of that type. If a property path that is specified isn't present in a result,
@@ -503,7 +631,7 @@
          * <p>Then, suppose that a query for "important" is issued with the following projection
          * type property paths:
          * <pre>{@code
-         * {schemaType: "Email", ["subject", "sender.name", "recipients.name"]}
+         * {schema: "Email", ["subject", "sender.name", "recipients.name"]}
          * }</pre>
          *
          * <p>The above document will be returned as:
@@ -526,58 +654,77 @@
          */
         @NonNull
         public SearchSpec.Builder addProjection(
-                @NonNull String schemaType, @NonNull String... propertyPaths) {
+                @NonNull String schema, @NonNull Collection<String> propertyPaths) {
+            Preconditions.checkNotNull(schema);
             Preconditions.checkNotNull(propertyPaths);
-            return addProjection(schemaType, Arrays.asList(propertyPaths));
-        }
-
-        /**
-         * Adds property paths for the specified type to be used for projection. If property
-         * paths are added for a type, then only the properties referred to will be retrieved for
-         * results of that type. If a property path that is specified isn't present in a result,
-         * it will be ignored for that result. Property paths cannot be null.
-         *
-         * <p>If no property paths are added for a particular type, then all properties of
-         * results of that type will be retrieved.
-         *
-         * <p>If property path is added for the
-         * {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will
-         * apply to all results, excepting any types that have their own, specific property paths
-         * set.
-         *
-         * {@see SearchSpec.Builder#addProjection(String, String...)}
-         */
-        @NonNull
-        public SearchSpec.Builder addProjection(
-                @NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(schemaType);
-            Preconditions.checkNotNull(propertyPaths);
+            resetIfBuilt();
             ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
             for (String propertyPath : propertyPaths) {
                 Preconditions.checkNotNull(propertyPath);
                 propertyPathsArrayList.add(propertyPath);
             }
-            mProjectionTypePropertyMasks.putStringArrayList(schemaType, propertyPathsArrayList);
+            mProjectionTypePropertyMasks.putStringArrayList(schema, propertyPathsArrayList);
             return this;
         }
 
         /**
-         * Constructs a new {@link SearchSpec} from the contents of this builder.
+         * Set the maximum number of results to return for each group, where groups are defined
+         * by grouping type.
          *
-         * <p>After calling this method, the builder must no longer be used.
+         * <p>Calling this method will override any previous calls. So calling
+         * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 7) and then calling
+         * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 2) will result in only the latter, a
+         * limit of two results per package, being applied. Or calling setResultGrouping
+         * (GROUPING_TYPE_PER_PACKAGE, 1) and then calling setResultGrouping
+         * (GROUPING_TYPE_PER_PACKAGE | GROUPING_PER_NAMESPACE, 5) will result in five results
+         * per package per namespace.
+         *
+         * @param groupingTypeFlags One or more combination of grouping types.
+         * @param limit             Number of results to return per {@code groupingTypeFlags}.
+         * @throws IllegalArgumentException if groupingTypeFlags is zero.
          */
+        // Individual parameters available from getResultGroupingTypeFlags and
+        // getResultGroupingLimit
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) {
+            Preconditions.checkState(
+                    groupingTypeFlags != 0, "Result grouping type cannot be zero.");
+            resetIfBuilt();
+            mGroupingTypeFlags = groupingTypeFlags;
+            mGroupingLimit = limit;
+            return this;
+        }
+
+        /** Constructs a new {@link SearchSpec} from the contents of this builder. */
         @NonNull
         public SearchSpec build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            if (!mBundle.containsKey(TERM_MATCH_TYPE_FIELD)) {
-                throw new IllegalSearchSpecException("Missing termMatchType field.");
-            }
-            mBundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
-            mBundle.putStringArrayList(SCHEMA_TYPE_FIELD, mSchemaTypes);
-            mBundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
+            Bundle bundle = new Bundle();
+            bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
+            bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
+            bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
+            bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
+            bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage);
+            bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType);
+            bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount);
+            bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty);
+            bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize);
+            bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
+            bundle.putInt(ORDER_FIELD, mOrder);
+            bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
+            bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
             mBuilt = true;
-            return new SearchSpec(mBundle);
+            return new SearchSpec(bundle);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mSchemas = new ArrayList<>(mSchemas);
+                mNamespaces = new ArrayList<>(mNamespaces);
+                mPackageNames = new ArrayList<>(mPackageNames);
+                mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
+                mBuilt = false;
+            }
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index 07df364..e09eec9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -18,6 +18,7 @@
 
 import android.annotation.SuppressLint;
 
+import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -36,44 +37,90 @@
 /**
  * Encapsulates a request to update the schema of an {@link AppSearchSession} database.
  *
+ * <p>The schema is composed of a collection of {@link AppSearchSchema} objects, each of which
+ * defines a unique type of data.
+ *
+ * <p>The first call to SetSchemaRequest will set the provided schema and store it within the
+ * {@link AppSearchSession} database.
+ *
+ * <p>Subsequent calls will compare the provided schema to the previously saved schema, to
+ * determine how to treat existing documents.
+ *
+ * <p>The following types of schema modifications are always safe and are made without deleting any
+ * existing documents:
+ * <ul>
+ *     <li>Addition of new {@link AppSearchSchema} types
+ *     <li>Addition of new properties to an existing {@link AppSearchSchema} type
+ *     <li>Changing the cardinality of a property to be less restrictive
+ * </ul>
+ *
+ * <p>The following types of schema changes are not backwards compatible:
+ * <ul>
+ *     <li>Removal of an existing {@link AppSearchSchema} type
+ *     <li>Removal of a property from an existing {@link AppSearchSchema} type
+ *     <li>Changing the data type of an existing property
+ *     <li>Changing the cardinality of a property to be more restrictive
+ * </ul>
+ *
+ * <p>Providing a schema with incompatible changes, will throw an
+ * {@link androidx.appsearch.exceptions.AppSearchException}, with a message describing the
+ * incompatibility. As a result, the previously set schema will remain unchanged.
+ *
+ * <p>Backward incompatible changes can be made by :
+ * <ul>
+ *     <li>setting {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}.
+ *         This deletes all documents that are incompatible with the new schema. The new schema is
+ *         then saved and persisted to disk.
+ *     <li>Add a {@link Migrator} for each incompatible type and make no deletion. The migrator
+ *         will migrate documents from it's old schema version to the new version. Migrated types
+ *         will be set into both {@link SetSchemaResponse#getIncompatibleTypes()} and
+ *         {@link SetSchemaResponse#getMigratedTypes()}. See the migration section below.
+ * </ul>
  * @see AppSearchSession#setSchema
+ * @see Migrator
  */
 public final class SetSchemaRequest {
     private final Set<AppSearchSchema> mSchemas;
-    private final Set<String> mSchemasNotVisibleToSystemUi;
+    private final Set<String> mSchemasNotDisplayedBySystem;
     private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+    private final Map<String, Migrator> mMigrators;
     private final boolean mForceOverride;
+    private final int mVersion;
 
     SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
-            @NonNull Set<String> schemasNotVisibleToSystemUi,
+            @NonNull Set<String> schemasNotDisplayedBySystem,
             @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
-            boolean forceOverride) {
+            @NonNull Map<String, Migrator> migrators,
+            boolean forceOverride,
+            int version) {
         mSchemas = Preconditions.checkNotNull(schemas);
-        mSchemasNotVisibleToSystemUi = Preconditions.checkNotNull(schemasNotVisibleToSystemUi);
+        mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem);
         mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages);
+        mMigrators = Preconditions.checkNotNull(migrators);
         mForceOverride = forceOverride;
+        mVersion = version;
     }
 
-    /** Returns the schemas that are part of this request. */
+    /** Returns the {@link AppSearchSchema} types that are part of this request. */
     @NonNull
     public Set<AppSearchSchema> getSchemas() {
         return Collections.unmodifiableSet(mSchemas);
     }
 
     /**
-     * Returns the set of schema types that have opted out of being visible on system UI surfaces.
+     * Returns all the schema types that are opted out of being displayed and visible on any
+     * system UI surface.
      */
     @NonNull
-    public Set<String> getSchemasNotVisibleToSystemUi() {
-        return Collections.unmodifiableSet(mSchemasNotVisibleToSystemUi);
+    public Set<String> getSchemasNotDisplayedBySystem() {
+        return Collections.unmodifiableSet(mSchemasNotDisplayedBySystem);
     }
 
     /**
      * Returns a mapping of schema types to the set of packages that have access
-     * to that schema type. Each package is represented by a {@link PackageIdentifier}.
-     * name and byte[] certificate.
+     * to that schema type.
      *
-     * This method is inefficient to call repeatedly.
+     * <p>It’s inefficient to call this method repeatedly.
      */
     @NonNull
     public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
@@ -85,11 +132,19 @@
     }
 
     /**
-     * Returns a mapping of schema types to the set of packages that have access
-     * to that schema type. Each package is represented by a {@link PackageIdentifier}.
-     * name and byte[] certificate.
+     * Returns the map of {@link Migrator}, the key will be the schema type of the
+     * {@link Migrator} associated with.
+     */
+    @NonNull
+    public Map<String, Migrator> getMigrators() {
+        return Collections.unmodifiableMap(mMigrators);
+    }
+
+    /**
+     * Returns a mapping of {@link AppSearchSchema} types to the set of packages that have access
+     * to that schema type.
      *
-     * A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
+     * <p>A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
      * modifiable map. This is not meant to be unhidden and should only be used by internal
      * classes.
      *
@@ -106,108 +161,139 @@
         return mForceOverride;
     }
 
+    /** Returns the database overall schema version. */
+    @IntRange(from = 1)
+    public int getVersion() {
+        return mVersion;
+    }
+
     /** Builder for {@link SetSchemaRequest} objects. */
     public static final class Builder {
-        private final Set<AppSearchSchema> mSchemas = new ArraySet<>();
-        private final Set<String> mSchemasNotVisibleToSystemUi = new ArraySet<>();
-        private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
+        private static final int DEFAULT_VERSION = 1;
+        private ArraySet<AppSearchSchema> mSchemas = new ArraySet<>();
+        private ArraySet<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
+        private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
                 new ArrayMap<>();
+        private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
         private boolean mForceOverride = false;
+        private int mVersion = DEFAULT_VERSION;
         private boolean mBuilt = false;
 
         /**
-         * Adds one or more types to the schema.
+         * Adds one or more {@link AppSearchSchema} types to the schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         * <p>An {@link AppSearchSchema} object represents one type of structured data.
+         *
+         * <p>Any documents of these types will be displayed on system UI surfaces by default.
          */
         @NonNull
-        public Builder addSchema(@NonNull AppSearchSchema... schemas) {
+        public Builder addSchemas(@NonNull AppSearchSchema... schemas) {
             Preconditions.checkNotNull(schemas);
-            return addSchema(Arrays.asList(schemas));
+            resetIfBuilt();
+            return addSchemas(Arrays.asList(schemas));
         }
 
         /**
-         * Adds one or more types to the schema.
+         * Adds a collection of {@link AppSearchSchema} objects to the schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         * <p>An {@link AppSearchSchema} object represents one type of structured data.
          */
         @NonNull
-        public Builder addSchema(@NonNull Collection<AppSearchSchema> schemas) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
+        public Builder addSchemas(@NonNull Collection<AppSearchSchema> schemas) {
             Preconditions.checkNotNull(schemas);
+            resetIfBuilt();
             mSchemas.addAll(schemas);
             return this;
         }
 
 // @exportToFramework:startStrip()
         /**
-         * Adds one or more types to the schema.
+         * Adds one or more {@link androidx.appsearch.annotation.Document} annotated classes to the
+         * schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
-         *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with
+         *                        {@link androidx.appsearch.annotation.Document}.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         *                            has not generated a schema for the given document classes.
          */
         @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemas()
         @NonNull
-        public Builder addDataClass(@NonNull Class<?>... dataClasses)
+        public Builder addDocumentClasses(@NonNull Class<?>... documentClasses)
                 throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            return addDataClass(Arrays.asList(dataClasses));
+            Preconditions.checkNotNull(documentClasses);
+            resetIfBuilt();
+            return addDocumentClasses(Arrays.asList(documentClasses));
         }
 
         /**
-         * Adds one or more types to the schema.
+         * Adds a collection of {@link androidx.appsearch.annotation.Document} annotated classes to
+         * the schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
-         *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with
+         *                        {@link androidx.appsearch.annotation.Document}.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         *                            has not generated a schema for the given document classes.
          */
         @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemas()
         @NonNull
-        public Builder addDataClass(@NonNull Collection<? extends Class<?>> dataClasses)
+        public Builder addDocumentClasses(@NonNull Collection<? extends Class<?>> documentClasses)
                 throws AppSearchException {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(dataClasses);
-            List<AppSearchSchema> schemas = new ArrayList<>(dataClasses.size());
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            for (Class<?> dataClass : dataClasses) {
-                DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+            Preconditions.checkNotNull(documentClasses);
+            resetIfBuilt();
+            List<AppSearchSchema> schemas = new ArrayList<>(documentClasses.size());
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            for (Class<?> documentClass : documentClasses) {
+                DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
                 schemas.add(factory.getSchema());
             }
-            return addSchema(schemas);
+            return addSchemas(schemas);
         }
 // @exportToFramework:endStrip()
 
         /**
-         * Sets visibility on system UI surfaces for the given {@code schemaType}.
+         * Sets whether or not documents from the provided {@code schemaType} will be displayed
+         * and visible on any system UI surface.
          *
-         * @param schemaType The schema type to set visibility on.
-         * @param visible    Whether the {@code schemaType} will be visible or not.
+         * <p>This setting applies to the provided {@code schemaType} only, and does not persist
+         * across {@link AppSearchSession#setSchema} calls.
+         *
+         * <p>The default behavior, if this method is not called, is to allow types to be
+         * displayed on system UI surfaces.
+         *
+         * @param schemaType The name of an {@link AppSearchSchema} within the same
+         *                   {@link SetSchemaRequest}, which will be configured.
+         * @param displayed  Whether documents of this type will be displayed on system UI surfaces.
          */
-        // Merged list available from getSchemasNotVisibleToSystemUi
+        // Merged list available from getSchemasNotDisplayedBySystem
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setSchemaTypeVisibilityForSystemUi(@NonNull String schemaType,
-                boolean visible) {
+        public Builder setSchemaTypeDisplayedBySystem(
+                @NonNull String schemaType, boolean displayed) {
             Preconditions.checkNotNull(schemaType);
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-
-            if (visible) {
-                mSchemasNotVisibleToSystemUi.remove(schemaType);
+            resetIfBuilt();
+            if (displayed) {
+                mSchemasNotDisplayedBySystem.remove(schemaType);
             } else {
-                mSchemasNotVisibleToSystemUi.add(schemaType);
+                mSchemasNotDisplayedBySystem.add(schemaType);
             }
             return this;
         }
 
         /**
-         * Sets visibility for a package for the given {@code schemaType}.
+         * Sets whether or not documents from the provided {@code schemaType} can be read by the
+         * specified package.
+         *
+         * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
+         * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+         *
+         * <p>To opt into one-way data sharing with another application, the developer will need to
+         * explicitly grant the other application’s package name and certificate Read access to its
+         * data.
+         *
+         * <p>For two-way data sharing, both applications need to explicitly grant Read access to
+         * one another.
+         *
+         * <p>By default, data sharing between applications is disabled.
          *
          * @param schemaType        The schema type to set visibility on.
          * @param visible           Whether the {@code schemaType} will be visible or not.
@@ -216,14 +302,15 @@
         // Merged list available from getSchemasVisibleToPackages
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setSchemaTypeVisibilityForPackage(@NonNull String schemaType,
-                boolean visible, @NonNull PackageIdentifier packageIdentifier) {
+        public Builder setSchemaTypeVisibilityForPackage(
+                @NonNull String schemaType,
+                boolean visible,
+                @NonNull PackageIdentifier packageIdentifier) {
             Preconditions.checkNotNull(schemaType);
             Preconditions.checkNotNull(packageIdentifier);
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            resetIfBuilt();
 
-            Set<PackageIdentifier> packageIdentifiers =
-                    mSchemasVisibleToPackages.get(schemaType);
+            Set<PackageIdentifier> packageIdentifiers = mSchemasVisibleToPackages.get(schemaType);
             if (visible) {
                 if (packageIdentifiers == null) {
                     packageIdentifiers = new ArraySet<>();
@@ -245,83 +332,212 @@
             return this;
         }
 
-// @exportToFramework:startStrip()
         /**
-         * Sets visibility on system UI surfaces for the given {@code dataClass}.
+         * Sets the {@link Migrator} associated with the given SchemaType.
          *
-         * @param dataClass The schema to set visibility on.
-         * @param visible   Whether the {@code schemaType} will be visible or not.
-         * @return {@link SetSchemaRequest.Builder}
-         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
+         * from the current version number stored in AppSearch to the final version set via
+         * {@link #setVersion}.
+         *
+         * <p>A {@link Migrator} will be invoked if the current version number stored in
+         * AppSearch is different from the final version set via {@link #setVersion} and
+         * {@link Migrator#shouldMigrate} returns {@code true}.
+         *
+         * <p>The target schema type of the output {@link GenericDocument} of
+         * {@link Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this
+         * {@link SetSchemaRequest}.
+         *
+         * @param schemaType The schema type to set migrator on.
+         * @param migrator   The migrator translates a document from its current version to the
+         *                   final version set via {@link #setVersion}.
+         *
+         * @see SetSchemaRequest.Builder#setVersion
+         * @see SetSchemaRequest.Builder#addSchemas
+         * @see AppSearchSession#setSchema
          */
-        // Merged list available from getSchemasNotVisibleToSystemUi
-        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setDataClassVisibilityForSystemUi(@NonNull Class<?> dataClass,
-                boolean visible) throws AppSearchException {
-            Preconditions.checkNotNull(dataClass);
-
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
-            return setSchemaTypeVisibilityForSystemUi(factory.getSchemaType(), visible);
+        @SuppressLint("MissingGetterMatchingBuilder")        // Getter return plural objects.
+        public Builder setMigrator(@NonNull String schemaType, @NonNull Migrator migrator) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(migrator);
+            resetIfBuilt();
+            mMigrators.put(schemaType, migrator);
+            return this;
         }
 
         /**
-         * Sets visibility for a package for the given {@code dataClass}.
+         * Sets a Map of {@link Migrator}s.
          *
-         * @param dataClass         The schema to set visibility on.
-         * @param visible           Whether the {@code schemaType} will be visible or not.
-         * @param packageIdentifier Represents the package that will be granted visibility
-         * @return {@link SetSchemaRequest.Builder}
+         * <p>The key of the map is the schema type that the {@link Migrator} value applies to.
+         *
+         * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
+         * from the current version number stored in AppSearch to the final version set via
+         * {@link #setVersion}.
+         *
+         * <p>A {@link Migrator} will be invoked if the current version number stored in
+         * AppSearch is different from the final version set via {@link #setVersion} and
+         * {@link Migrator#shouldMigrate} returns {@code true}.
+         *
+         * <p>The target schema type of the output {@link GenericDocument} of
+         * {@link Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this
+         * {@link SetSchemaRequest}.
+         *
+         * @param migrators  A {@link Map} of migrators that translate a document from it's current
+         *                   version to the final version set via {@link #setVersion}. The key of
+         *                   the map is the schema type that the {@link Migrator} value applies to.
+         *
+         * @see SetSchemaRequest.Builder#setVersion
+         * @see SetSchemaRequest.Builder#addSchemas
+         * @see AppSearchSession#setSchema
+         */
+        @NonNull
+        public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
+            Preconditions.checkNotNull(migrators);
+            resetIfBuilt();
+            mMigrators.putAll(migrators);
+            return this;
+        }
+
+// @exportToFramework:startStrip()
+
+        /**
+         * Sets whether or not documents from the provided
+         * {@link androidx.appsearch.annotation.Document} annotated class will be displayed and
+         * visible on any system UI surface.
+         *
+         * <p>This setting applies to the provided {@link androidx.appsearch.annotation.Document}
+         * annotated class only, and does not persist across {@link AppSearchSession#setSchema}
+         * calls.
+         *
+         * <p>The default behavior, if this method is not called, is to allow types to be
+         * displayed on system UI surfaces.
+         *
+         * @param documentClass A class annotated with
+         *                      {@link androidx.appsearch.annotation.Document}, the visibility of
+         *                      which will be configured
+         * @param displayed     Whether documents of this type will be displayed on system UI
+         *                      surfaces.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         *                            has not generated a schema for the given document class.
+         */
+        // Merged list available from getSchemasNotDisplayedBySystem
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setDocumentClassDisplayedBySystem(@NonNull Class<?> documentClass,
+                boolean displayed) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return setSchemaTypeDisplayedBySystem(factory.getSchemaName(), displayed);
+        }
+
+        /**
+         * Sets whether or not documents from the provided
+         * {@link androidx.appsearch.annotation.Document} annotated class can be read by the
+         * specified package.
+         *
+         * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
+         * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+         *
+         * <p>To opt into one-way data sharing with another application, the developer will need to
+         * explicitly grant the other application’s package name and certificate Read access to its
+         * data.
+         *
+         * <p>For two-way data sharing, both applications need to explicitly grant Read access to
+         * one another.
+         *
+         * <p>By default, app data sharing between applications is disabled.
+         *
+         * @param documentClass     The {@link androidx.appsearch.annotation.Document} class to set
+         *                          visibility on.
+         * @param visible           Whether the {@code documentClass} will be visible or not.
+         * @param packageIdentifier Represents the package that will be granted visibility.
+         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+         *                            has not generated a schema for the given document class.
          */
         // Merged list available from getSchemasVisibleToPackages
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setDataClassVisibilityForPackage(@NonNull Class<?> dataClass,
+        public Builder setDocumentClassVisibilityForPackage(@NonNull Class<?> documentClass,
                 boolean visible, @NonNull PackageIdentifier packageIdentifier)
                 throws AppSearchException {
-            Preconditions.checkNotNull(dataClass);
-
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
-            return setSchemaTypeVisibilityForPackage(factory.getSchemaType(), visible,
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return setSchemaTypeVisibilityForPackage(factory.getSchemaName(), visible,
                     packageIdentifier);
         }
 // @exportToFramework:endStrip()
 
         /**
-         * Configures the {@link SetSchemaRequest} to delete any existing documents that don't
-         * follow the new schema.
+         * Sets whether or not to override the current schema in the {@link AppSearchSession}
+         * database.
          *
-         * <p>By default, this is {@code false} and schema incompatibility causes the
-         * {@link AppSearchSession#setSchema} call to fail.
+         * <p>Call this method whenever backward incompatible changes need to be made by setting
+         * {@code forceOverride} to {@code true}. As a result, during execution of the setSchema
+         * operation, all documents that are incompatible with the new schema will be deleted and
+         * the new schema will be saved and persisted.
          *
-         * @see AppSearchSession#setSchema
+         * <p>By default, this is {@code false}.
          */
         @NonNull
         public Builder setForceOverride(boolean forceOverride) {
+            resetIfBuilt();
             mForceOverride = forceOverride;
             return this;
         }
 
         /**
-         * Builds a new {@link SetSchemaRequest}.
+         * Sets the version number of the overall {@link AppSearchSchema} in the database.
          *
-         * @throws IllegalArgumentException If schema types were referenced, but the
-         *                                  corresponding {@link AppSearchSchema} was never added.
+         * <p>The {@link AppSearchSession} database can only ever hold documents for one version
+         * at a time.
+         *
+         * <p>Setting a version number that is different from the version number currently stored
+         * in AppSearch will result in AppSearch calling the {@link Migrator}s provided to
+         * {@link AppSearchSession#setSchema} to migrate the documents already in AppSearch from
+         * the previous version to the one set in this request. The version number can be
+         * updated without any other changes to the set of schemas.
+         *
+         * <p>The version number can stay the same, increase, or decrease relative to the current
+         * version number that is already stored in the {@link AppSearchSession} database.
+         *
+         * <p>The version of an empty database will always be 0. You cannot set version to the
+         * {@link SetSchemaRequest}, if it doesn't contains any {@link AppSearchSchema}.
+         *
+         * @param version A positive integer representing the version of the entire set of
+         *                schemas represents the version of the whole schema in the
+         *                {@link AppSearchSession} database, default version is 1.
+         *
+         * @throws IllegalArgumentException if the version is negative.
+         *
+         * @see AppSearchSession#setSchema
+         * @see Migrator
+         * @see SetSchemaRequest.Builder#setMigrator
+         */
+        @NonNull
+        public Builder setVersion(@IntRange(from = 1) int version) {
+            Preconditions.checkArgument(version >= 1, "Version must be a positive number.");
+            resetIfBuilt();
+            mVersion = version;
+            return this;
+        }
+
+        /**
+         * Builds a new {@link SetSchemaRequest} object.
+         *
+         * @throws IllegalArgumentException if schema types were referenced, but the
+         *                                  corresponding {@link AppSearchSchema} type was never
+         *                                  added.
          */
         @NonNull
         public SetSchemaRequest build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mBuilt = true;
-
-            // Verify that any schema types with visibility settings refer to a real schema.
+            // Verify that any schema types with display or visibility settings refer to a real
+            // schema.
             // Create a copy because we're going to remove from the set for verification purposes.
-            Set<String> referencedSchemas = new ArraySet<>(
-                    mSchemasNotVisibleToSystemUi);
+            Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
             referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
 
             for (AppSearchSchema schema : mSchemas) {
@@ -331,13 +547,37 @@
                 // We still have schema types that weren't seen in our mSchemas set. This means
                 // there wasn't a corresponding AppSearchSchema.
                 throw new IllegalArgumentException(
-                        "Schema types " + referencedSchemas
-                                + " referenced, but were not added.");
+                        "Schema types " + referencedSchemas + " referenced, but were not added.");
             }
-
-            return new SetSchemaRequest(mSchemas, mSchemasNotVisibleToSystemUi,
+            if (mSchemas.isEmpty() && mVersion != DEFAULT_VERSION) {
+                throw new IllegalArgumentException(
+                        "Cannot set version to the request if schema is empty.");
+            }
+            mBuilt = true;
+            return new SetSchemaRequest(
+                    mSchemas,
+                    mSchemasNotDisplayedBySystem,
                     mSchemasVisibleToPackages,
-                    mForceOverride);
+                    mMigrators,
+                    mForceOverride,
+                    mVersion);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                ArrayMap<String, Set<PackageIdentifier>> schemasVisibleToPackages =
+                        new ArrayMap<>(mSchemasVisibleToPackages.size());
+                for (Map.Entry<String, Set<PackageIdentifier>> entry
+                        : mSchemasVisibleToPackages.entrySet()) {
+                    schemasVisibleToPackages.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+                }
+                mSchemasVisibleToPackages = schemasVisibleToPackages;
+
+                mSchemas = new ArraySet<>(mSchemas);
+                mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
+                mMigrators = new ArrayMap<>(mMigrators);
+                mBuilt = false;
+            }
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
new file mode 100644
index 0000000..3290dd1
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/** The response class of {@link AppSearchSession#setSchema} */
+public class SetSchemaResponse {
+
+    private static final String DELETED_TYPES_FIELD = "deletedTypes";
+    private static final String INCOMPATIBLE_TYPES_FIELD = "incompatibleTypes";
+    private static final String MIGRATED_TYPES_FIELD = "migratedTypes";
+
+    private final Bundle mBundle;
+    /**
+     * The migrationFailures won't be saved in the bundle. Since:
+     * <ul>
+     *     <li>{@link MigrationFailure} is generated in {@link AppSearchSession} which will be
+     *         the SDK side in platform. We don't need to pass it from service side via binder.
+     *     <li>Translate multiple {@link MigrationFailure}s to bundles in {@link Builder} and then
+     *         back in constructor will be a huge waste.
+     * </ul>
+     */
+    private final List<MigrationFailure> mMigrationFailures;
+
+    /** Cache of the inflated deleted schema types. Comes from inflating mBundles at first use. */
+    @Nullable
+    private Set<String> mDeletedTypes;
+
+    /** Cache of the inflated migrated schema types. Comes from inflating mBundles at first use. */
+    @Nullable
+    private Set<String> mMigratedTypes;
+
+    /**
+     * Cache of the inflated incompatible schema types. Comes from inflating mBundles at first use.
+     */
+    @Nullable
+    private Set<String> mIncompatibleTypes;
+
+    SetSchemaResponse(@NonNull Bundle bundle, @NonNull List<MigrationFailure> migrationFailures) {
+        mBundle = Preconditions.checkNotNull(bundle);
+        mMigrationFailures = Preconditions.checkNotNull(migrationFailures);
+    }
+
+    SetSchemaResponse(@NonNull Bundle bundle) {
+        this(bundle, /*migrationFailures=*/ Collections.emptyList());
+    }
+
+    /**
+     * Returns the {@link Bundle} populated by this builder.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Returns a {@link List} of all failed {@link MigrationFailure}.
+     *
+     * <p>A {@link MigrationFailure} will be generated if the system trying to save a post-migrated
+     * {@link GenericDocument} but fail.
+     *
+     * <p>{@link MigrationFailure} contains the namespace, id and schemaType of the post-migrated
+     * {@link GenericDocument} and the error reason. Mostly it will be mismatch the schema it
+     * migrated to.
+     */
+    @NonNull
+    public List<MigrationFailure> getMigrationFailures() {
+        return Collections.unmodifiableList(mMigrationFailures);
+    }
+
+    /**
+     * Returns a {@link Set} of deleted schema types.
+     *
+     * <p>A "deleted" type is a schema type that was previously a part of the database schema but
+     * was not present in the {@link SetSchemaRequest} object provided in the
+     * {@link AppSearchSession#setSchema) call.
+     *
+     * <p>Documents for a deleted type are removed from the database.
+     */
+    @NonNull
+    public Set<String> getDeletedTypes() {
+        if (mDeletedTypes == null) {
+            mDeletedTypes = new ArraySet<>(
+                    Preconditions.checkNotNull(mBundle.getStringArrayList(DELETED_TYPES_FIELD)));
+        }
+        return Collections.unmodifiableSet(mDeletedTypes);
+    }
+
+    /**
+     * Returns a {@link Set} of schema type that were migrated by the
+     * {@link AppSearchSession#setSchema} call.
+     *
+     * <p> A "migrated" type is a schema type that has triggered a {@link Migrator} instance to
+     * migrate documents of the schema type to another schema type, or to another version of the
+     * schema type.
+     *
+     * <p>If a document fails to be migrated, a {@link MigrationFailure} will be generated
+     * for that document.
+     *
+     * @see Migrator
+     */
+    @NonNull
+    public Set<String> getMigratedTypes() {
+        if (mMigratedTypes == null) {
+            mMigratedTypes = new ArraySet<>(
+                    Preconditions.checkNotNull(mBundle.getStringArrayList(MIGRATED_TYPES_FIELD)));
+        }
+        return Collections.unmodifiableSet(mMigratedTypes);
+    }
+
+    /**
+     * Returns a {@link Set} of schema type whose new definitions set in the
+     * {@link AppSearchSession#setSchema} call were incompatible with the pre-existing schema.
+     *
+     * <p>If a {@link Migrator} is provided for this type and the migration is success triggered.
+     * The type will also appear in {@link #getMigratedTypes()}.
+     *
+     * @see SetSchemaRequest
+     * @see AppSearchSession#setSchema
+     * @see SetSchemaRequest.Builder#setForceOverride
+     */
+    @NonNull
+    public Set<String> getIncompatibleTypes() {
+        if (mIncompatibleTypes == null) {
+            mIncompatibleTypes = new ArraySet<>(
+                    Preconditions.checkNotNull(
+                            mBundle.getStringArrayList(INCOMPATIBLE_TYPES_FIELD)));
+        }
+        return Collections.unmodifiableSet(mIncompatibleTypes);
+    }
+
+    /**
+     * Translates the {@link SetSchemaResponse}'s bundle to {@link Builder}.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    // TODO(b/179302942) change to Builder(mBundle) powered by mBundle.deepCopy
+    public Builder toBuilder() {
+        return new Builder()
+                .addDeletedTypes(getDeletedTypes())
+                .addIncompatibleTypes(getIncompatibleTypes())
+                .addMigratedTypes(getMigratedTypes())
+                .addMigrationFailures(mMigrationFailures);
+    }
+
+    /** Builder for {@link SetSchemaResponse} objects. */
+    public static final class Builder {
+        private List<MigrationFailure> mMigrationFailures = new ArrayList<>();
+        private ArrayList<String> mDeletedTypes = new ArrayList<>();
+        private ArrayList<String> mMigratedTypes = new ArrayList<>();
+        private ArrayList<String> mIncompatibleTypes = new ArrayList<>();
+        private boolean mBuilt = false;
+
+        /**  Adds {@link MigrationFailure}s to the list of migration failures. */
+        @NonNull
+        public Builder addMigrationFailures(
+                @NonNull Collection<MigrationFailure> migrationFailures) {
+            Preconditions.checkNotNull(migrationFailures);
+            resetIfBuilt();
+            mMigrationFailures.addAll(migrationFailures);
+            return this;
+        }
+
+        /**  Adds a {@link MigrationFailure} to the list of migration failures. */
+        @NonNull
+        public Builder addMigrationFailure(@NonNull MigrationFailure migrationFailure) {
+            Preconditions.checkNotNull(migrationFailure);
+            resetIfBuilt();
+            mMigrationFailures.add(migrationFailure);
+            return this;
+        }
+
+        /**  Adds deletedTypes to the list of deleted schema types. */
+        @NonNull
+        public Builder addDeletedTypes(@NonNull Collection<String> deletedTypes) {
+            Preconditions.checkNotNull(deletedTypes);
+            resetIfBuilt();
+            mDeletedTypes.addAll(deletedTypes);
+            return this;
+        }
+
+        /**  Adds one deletedType to the list of deleted schema types. */
+        @NonNull
+        public Builder addDeletedType(@NonNull String deletedType) {
+            Preconditions.checkNotNull(deletedType);
+            resetIfBuilt();
+            mDeletedTypes.add(deletedType);
+            return this;
+        }
+
+        /**  Adds incompatibleTypes to the list of incompatible schema types. */
+        @NonNull
+        public Builder addIncompatibleTypes(@NonNull Collection<String> incompatibleTypes) {
+            Preconditions.checkNotNull(incompatibleTypes);
+            resetIfBuilt();
+            mIncompatibleTypes.addAll(incompatibleTypes);
+            return this;
+        }
+
+        /**  Adds one incompatibleType to the list of incompatible schema types. */
+        @NonNull
+        public Builder addIncompatibleType(@NonNull String incompatibleType) {
+            Preconditions.checkNotNull(incompatibleType);
+            resetIfBuilt();
+            mIncompatibleTypes.add(incompatibleType);
+            return this;
+        }
+
+        /**  Adds migratedTypes to the list of migrated schema types. */
+        @NonNull
+        public Builder addMigratedTypes(@NonNull Collection<String> migratedTypes) {
+            Preconditions.checkNotNull(migratedTypes);
+            resetIfBuilt();
+            mMigratedTypes.addAll(migratedTypes);
+            return this;
+        }
+
+        /**  Adds one migratedType to the list of migrated schema types. */
+        @NonNull
+        public Builder addMigratedType(@NonNull String migratedType) {
+            Preconditions.checkNotNull(migratedType);
+            resetIfBuilt();
+            mMigratedTypes.add(migratedType);
+            return this;
+        }
+
+        /** Builds a {@link SetSchemaResponse} object. */
+        @NonNull
+        public SetSchemaResponse build() {
+            Bundle bundle = new Bundle();
+            bundle.putStringArrayList(INCOMPATIBLE_TYPES_FIELD, mIncompatibleTypes);
+            bundle.putStringArrayList(DELETED_TYPES_FIELD, mDeletedTypes);
+            bundle.putStringArrayList(MIGRATED_TYPES_FIELD, mMigratedTypes);
+            mBuilt = true;
+            // Avoid converting the potential thousands of MigrationFailures to Pracelable and
+            // back just for put in bundle. In platform, we should set MigrationFailures in
+            // AppSearchSession after we pass SetSchemaResponse via binder.
+            return new SetSchemaResponse(bundle, mMigrationFailures);
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mMigrationFailures = new ArrayList<>(mMigrationFailures);
+                mDeletedTypes = new ArrayList<>(mDeletedTypes);
+                mMigratedTypes = new ArrayList<>(mMigratedTypes);
+                mIncompatibleTypes = new ArrayList<>(mIncompatibleTypes);
+                mBuilt = false;
+            }
+        }
+    }
+
+    /**
+     * The class represents a post-migrated {@link GenericDocument} that failed to be saved by
+     * {@link AppSearchSession#setSchema}.
+     */
+    public static class MigrationFailure {
+        private static final String SCHEMA_TYPE_FIELD = "schemaType";
+        private static final String NAMESPACE_FIELD = "namespace";
+        private static final String DOCUMENT_ID_FIELD = "id";
+        private static final String ERROR_MESSAGE_FIELD = "errorMessage";
+        private static final String RESULT_CODE_FIELD = "resultCode";
+
+        private final Bundle mBundle;
+
+        /**
+         * Constructs a new {@link MigrationFailure}.
+         *
+         * @param namespace    The namespace of the document which failed to be migrated.
+         * @param documentId   The id of the document which failed to be migrated.
+         * @param schemaType   The type of the document which failed to be migrated.
+         * @param failedResult The reason why the document failed to be indexed.
+         * @throws IllegalArgumentException if the provided {@code failedResult} was not a failure.
+         */
+        public MigrationFailure(
+                @NonNull String namespace,
+                @NonNull String documentId,
+                @NonNull String schemaType,
+                @NonNull AppSearchResult<?> failedResult) {
+            mBundle = new Bundle();
+            mBundle.putString(NAMESPACE_FIELD, Preconditions.checkNotNull(namespace));
+            mBundle.putString(DOCUMENT_ID_FIELD, Preconditions.checkNotNull(documentId));
+            mBundle.putString(SCHEMA_TYPE_FIELD, Preconditions.checkNotNull(schemaType));
+
+            Preconditions.checkNotNull(failedResult);
+            Preconditions.checkArgument(
+                    !failedResult.isSuccess(), "failedResult was actually successful");
+            mBundle.putString(ERROR_MESSAGE_FIELD, failedResult.getErrorMessage());
+            mBundle.putInt(RESULT_CODE_FIELD, failedResult.getResultCode());
+        }
+
+        MigrationFailure(@NonNull Bundle bundle) {
+            mBundle = Preconditions.checkNotNull(bundle);
+        }
+
+        /**
+         * Returns the Bundle of the {@link MigrationFailure}.
+         *
+         * @hide
+         */
+        @NonNull
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Bundle getBundle() {
+            return mBundle;
+        }
+
+        /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */
+        @NonNull
+        public String getNamespace() {
+            return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/"");
+        }
+
+        /**
+         * @deprecated TODO(b/181887768): Exists for dogfood transition; must be removed.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Deprecated
+        /*@exportToFramework:UnsupportedAppUsage*/
+        @NonNull
+        public String getUri() {
+            return getDocumentId();
+        }
+
+        /** Returns the id of the {@link GenericDocument} that failed to be migrated. */
+        @NonNull
+        public String getDocumentId() {
+            return mBundle.getString(DOCUMENT_ID_FIELD, /*defaultValue=*/"");
+        }
+
+        /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */
+        @NonNull
+        public String getSchemaType() {
+            return mBundle.getString(SCHEMA_TYPE_FIELD, /*defaultValue=*/"");
+        }
+
+        /**
+         * Returns the {@link AppSearchResult} that indicates why the
+         * post-migration {@link GenericDocument} failed to be indexed.
+         */
+        @NonNull
+        public AppSearchResult<Void> getAppSearchResult() {
+            return AppSearchResult.newFailedResult(mBundle.getInt(RESULT_CODE_FIELD),
+                    mBundle.getString(ERROR_MESSAGE_FIELD, /*defaultValue=*/""));
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return "MigrationFailure { schemaType: " + getSchemaType() + ", namespace: "
+                    + getNamespace() + ", documentId: " + getDocumentId() + ", appSearchResult: "
+                    + getAppSearchResult().toString() + "}";
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
new file mode 100644
index 0000000..0d7901d
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/** The response class of {@code AppSearchSession#getStorageInfo}. */
+public class StorageInfo {
+
+    private static final String SIZE_BYTES_FIELD = "sizeBytes";
+    private static final String ALIVE_DOCUMENTS_COUNT = "aliveDocumentsCount";
+    private static final String ALIVE_NAMESPACES_COUNT = "aliveNamespacesCount";
+
+    private final Bundle mBundle;
+
+    StorageInfo(@NonNull Bundle bundle) {
+        mBundle = Preconditions.checkNotNull(bundle);
+    }
+
+    /**
+     * Returns the {@link Bundle} populated by this builder.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /** Returns the estimated size of the session's database in bytes. */
+    public long getSizeBytes() {
+        return mBundle.getLong(SIZE_BYTES_FIELD);
+    }
+
+    /**
+     * Returns the number of alive documents in the current session.
+     *
+     * <p>Alive documents are documents that haven't been deleted and haven't exceeded the ttl as
+     * set in {@link GenericDocument.Builder#setTtlMillis}.
+     */
+    public int getAliveDocumentsCount() {
+        return mBundle.getInt(ALIVE_DOCUMENTS_COUNT);
+    }
+
+    /**
+     * Returns the number of namespaces that have at least one alive document in the current
+     * session's database.
+     *
+     * <p>Alive documents are documents that haven't been deleted and haven't exceeded the ttl as
+     * set in {@link GenericDocument.Builder#setTtlMillis}.
+     */
+    public int getAliveNamespacesCount() {
+        return mBundle.getInt(ALIVE_NAMESPACES_COUNT);
+    }
+
+    /** Builder for {@link StorageInfo} objects. */
+    public static final class Builder {
+        private long mSizeBytes;
+        private int mAliveDocumentsCount;
+        private int mAliveNamespacesCount;
+
+        /** Sets the size in bytes. */
+        @NonNull
+        public StorageInfo.Builder setSizeBytes(long sizeBytes) {
+            mSizeBytes = sizeBytes;
+            return this;
+        }
+
+        /** Sets the number of alive documents. */
+        @NonNull
+        public StorageInfo.Builder setAliveDocumentsCount(int aliveDocumentsCount) {
+            mAliveDocumentsCount = aliveDocumentsCount;
+            return this;
+        }
+
+        /** Sets the number of alive namespaces. */
+        @NonNull
+        public StorageInfo.Builder setAliveNamespacesCount(int aliveNamespacesCount) {
+            mAliveNamespacesCount = aliveNamespacesCount;
+            return this;
+        }
+
+        /** Builds a {@link StorageInfo} object. */
+        @NonNull
+        public StorageInfo build() {
+            Bundle bundle = new Bundle();
+            bundle.putLong(SIZE_BYTES_FIELD, mSizeBytes);
+            bundle.putInt(ALIVE_DOCUMENTS_COUNT, mAliveDocumentsCount);
+            bundle.putInt(ALIVE_NAMESPACES_COUNT, mAliveNamespacesCount);
+            return new StorageInfo(bundle);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
index 262c97e..98689f5 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
@@ -31,19 +31,35 @@
 
     /**
      * Initializes an {@link AppSearchException} with no message.
-     * @hide
+     *
+     * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
      */
     public AppSearchException(@AppSearchResult.ResultCode int resultCode) {
         this(resultCode, /*message=*/ null);
     }
 
-    /** @hide */
+    /**
+     * Initializes an {@link AppSearchException} with a result code and message.
+     *
+     * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+     * @param message    The detail message (which is saved for later retrieval by the
+     *                   {@link #getMessage()} method).
+     */
     public AppSearchException(
             @AppSearchResult.ResultCode int resultCode, @Nullable String message) {
         this(resultCode, message, /*cause=*/ null);
     }
 
-    /** @hide */
+    /**
+     * Initializes an {@link AppSearchException} with a result code, message and cause.
+     *
+     * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+     * @param message    The detail message (which is saved for later retrieval by the
+     *                   {@link #getMessage()} method).
+     * @param cause      The cause (which is saved for later retrieval by the {@link #getCause()}
+     *                   method). (A null value is permitted, and indicates that the cause is
+     *                   nonexistent or unknown.)
+     */
     public AppSearchException(
             @AppSearchResult.ResultCode int resultCode,
             @Nullable String message,
@@ -52,14 +68,16 @@
         mResultCode = resultCode;
     }
 
-    /** Returns the result code this exception was constructed with. */
+    /**
+     * Returns the result code this exception was constructed with.
+     *
+     * @return One of the constants documented in {@link AppSearchResult#getResultCode}.
+     */
     public @AppSearchResult.ResultCode int getResultCode() {
         return mResultCode;
     }
 
-    /**
-     * Converts this {@link java.lang.Exception} into a failed {@link AppSearchResult}
-     */
+    /** Converts this {@link java.lang.Exception} into a failed {@link AppSearchResult}. */
     @NonNull
     public <T> AppSearchResult<T> toAppSearchResult() {
         return AppSearchResult.newFailedResult(mResultCode, getMessage());
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/IllegalSearchSpecException.java b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/IllegalSearchSpecException.java
deleted file mode 100644
index 3e06f81..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/IllegalSearchSpecException.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 androidx.appsearch.exceptions;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-/**
- * Indicates that a {@link androidx.appsearch.app.SearchResult} has logical inconsistencies such
- * as unpopulated mandatory fields or illegal combinations of parameters.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class IllegalSearchSpecException extends IllegalArgumentException {
-    /**
-     * Constructs a new {@link IllegalSearchSpecException}.
-     *
-     * @param message A developer-readable description of the issue with the bundle.
-     */
-    public IllegalSearchSpecException(@NonNull String message) {
-        super(message);
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
index 86de233..bf22395 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
@@ -17,8 +17,10 @@
 package androidx.appsearch.util;
 
 import android.os.Bundle;
+import android.os.Parcel;
 import android.util.SparseArray;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 
@@ -148,35 +150,41 @@
         if (bundle == null) {
             return 0;
         }
-        int[] hashCodes = new int[bundle.size()];
-        int i = 0;
+        int[] hashCodes = new int[bundle.size() + 1];
+        int hashCodeIdx = 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);
+        // Because bundle.keySet() doesn't guarantee any particular order, we need to sort the keys
+        // in case the iteration order varies from run to run.
+        String[] keys = bundle.keySet().toArray(new String[0]);
+        Arrays.sort(keys);
+        // Hash the keys so we can detect key-only differences
+        hashCodes[hashCodeIdx++] = Arrays.hashCode(keys);
+        for (int keyIdx = 0; keyIdx < keys.length; keyIdx++) {
+            Object value = bundle.get(keys[keyIdx]);
             if (value instanceof Bundle) {
-                hashCodes[i++] = deepHashCode((Bundle) value);
+                hashCodes[hashCodeIdx++] = deepHashCode((Bundle) value);
             } else if (value instanceof int[]) {
-                hashCodes[i++] = Arrays.hashCode((int[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((int[]) value);
             } else if (value instanceof byte[]) {
-                hashCodes[i++] = Arrays.hashCode((byte[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((byte[]) value);
             } else if (value instanceof char[]) {
-                hashCodes[i++] = Arrays.hashCode((char[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((char[]) value);
             } else if (value instanceof long[]) {
-                hashCodes[i++] = Arrays.hashCode((long[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((long[]) value);
             } else if (value instanceof float[]) {
-                hashCodes[i++] = Arrays.hashCode((float[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((float[]) value);
             } else if (value instanceof short[]) {
-                hashCodes[i++] = Arrays.hashCode((short[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((short[]) value);
             } else if (value instanceof double[]) {
-                hashCodes[i++] = Arrays.hashCode((double[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((double[]) value);
             } else if (value instanceof boolean[]) {
-                hashCodes[i++] = Arrays.hashCode((boolean[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((boolean[]) value);
             } else if (value instanceof String[]) {
                 // Optimization to avoid Object[] handler creating an inner array for common cases
-                hashCodes[i++] = Arrays.hashCode((String[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((String[]) value);
             } else if (value instanceof Object[]) {
                 Object[] array = (Object[]) value;
                 int[] innerHashCodes = new int[array.length];
@@ -187,7 +195,7 @@
                         innerHashCodes[j] = array[j].hashCode();
                     }
                 }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
             } else if (value instanceof ArrayList) {
                 ArrayList<?> list = (ArrayList<?>) value;
                 int[] innerHashCodes = new int[list.size()];
@@ -199,7 +207,7 @@
                         innerHashCodes[j] = item.hashCode();
                     }
                 }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
             } else if (value instanceof SparseArray) {
                 SparseArray<?> array = (SparseArray<?>) value;
                 int[] innerHashCodes = new int[array.size() * 2];
@@ -212,11 +220,33 @@
                         innerHashCodes[j * 2 + 1] = item.hashCode();
                     }
                 }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
             } else {
-                hashCodes[i++] = value.hashCode();
+                hashCodes[hashCodeIdx++] = value.hashCode();
             }
         }
         return Arrays.hashCode(hashCodes);
     }
+
+    /**
+     * Deeply clones a Bundle.
+     *
+     * <p>Values which are Bundles, Lists or Arrays are deeply copied themselves.
+     */
+    @NonNull
+    public static Bundle deepCopy(@NonNull Bundle bundle) {
+        // Write bundle to bytes
+        Parcel parcel = Parcel.obtain();
+        try {
+            parcel.writeBundle(bundle);
+            byte[] serializedMessage = parcel.marshall();
+
+            // Read bundle from bytes
+            parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
+            parcel.setDataPosition(0);
+            return parcel.readBundle();
+        } finally {
+            parcel.recycle();
+        }
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/IndentingStringBuilder.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/IndentingStringBuilder.java
new file mode 100644
index 0000000..ea5717e
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/IndentingStringBuilder.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Utility for building indented strings.
+ *
+ * <p>This is a wrapper for {@link StringBuilder} for appending strings with indentation.
+ * The indentation level can be increased by calling {@link #increaseIndentLevel()} and decreased
+ * by calling {@link #decreaseIndentLevel()}.
+ *
+ * <p>Indentation is applied after each newline character for the given indent level.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class IndentingStringBuilder {
+    private final StringBuilder mStringBuilder = new StringBuilder();
+
+    // Indicates whether next non-newline character should have an indent applied before it.
+    private boolean mIndentNext = false;
+    private int mIndentLevel = 0;
+
+    /**
+     * Increases the indent level by one for appended strings.
+     */
+    @NonNull
+    public IndentingStringBuilder increaseIndentLevel() {
+        mIndentLevel++;
+        return this;
+    }
+
+    /**
+     * Decreases the indent level by one for appended strings.
+     */
+    @NonNull
+    public IndentingStringBuilder decreaseIndentLevel() throws IllegalStateException {
+        if (mIndentLevel == 0) {
+            throw new IllegalStateException("Cannot set indent level below 0.");
+        }
+        mIndentLevel--;
+        return this;
+    }
+
+    /**
+     * Appends provided {@code String} at the current indentation level.
+     *
+     * <p>Indentation is applied after each newline character.
+     */
+    @NonNull
+    public IndentingStringBuilder append(@NonNull String str) {
+        applyIndentToString(str);
+        return this;
+    }
+
+    /**
+     * Appends provided {@code Object}, represented as a {@code String}, at the current indentation
+     * level.
+     *
+     * <p>Indentation is applied after each newline character.
+     */
+    @NonNull
+    public IndentingStringBuilder append(@NonNull Object obj) {
+        applyIndentToString(obj.toString());
+        return this;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return mStringBuilder.toString();
+    }
+
+    /**
+     * Adds indent string to the {@link StringBuilder} instance for current indent level.
+     */
+    private void applyIndent() {
+        for (int i = 0; i < mIndentLevel; i++) {
+            mStringBuilder.append("  ");
+        }
+    }
+
+    /**
+     * Applies indent, for current indent level, after each newline character.
+     *
+     * <p>Consecutive newline characters are not indented.
+     */
+    private void applyIndentToString(@NonNull String str) {
+        int index = str.indexOf("\n");
+        if (index == 0) {
+            // String begins with new line character: append newline and slide past newline.
+            mStringBuilder.append("\n");
+            mIndentNext = true;
+            if (str.length() > 1) {
+                applyIndentToString(str.substring(index + 1));
+            }
+        } else if (index >= 1) {
+            // String contains new line character: divide string between newline, append new line,
+            // and recurse on each string.
+            String beforeIndentString = str.substring(0, index);
+            applyIndentToString(beforeIndentString);
+            mStringBuilder.append("\n");
+            mIndentNext = true;
+            if (str.length() > index + 1) {
+                String afterIndentString = str.substring(index + 1);
+                applyIndentToString(afterIndentString);
+            }
+        } else {
+            // String does not contain newline character: append string.
+            if (mIndentNext) {
+                applyIndent();
+                mIndentNext = false;
+            }
+            mStringBuilder.append(str);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
new file mode 100644
index 0000000..b360ea0
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.util;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Utilities for logging to logcat.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class LogUtil {
+    /**
+     * The {@link #piiTrace} logs are intended for sensitive data that can't be enabled in
+     * production, so they are build-gated by this constant.
+     *
+     * <p><ul>
+     * <li>0: no tracing.
+     * <li>1: fast tracing (statuses/counts only)
+     * <li>2: full tracing (complete messages)
+     * </ul>
+     */
+    private static final int PII_TRACE_LEVEL = 0;
+
+    private final String mTag;
+
+    public LogUtil(@NonNull String tag) {
+        mTag = Preconditions.checkNotNull(tag);
+    }
+
+    /** Returns whether piiTrace() is enabled (PII_TRACE_LEVEL > 0). */
+    public boolean isPiiTraceEnabled() {
+        return PII_TRACE_LEVEL > 0;
+    }
+
+    /**
+     * If icing lib interaction tracing is enabled via {@link #PII_TRACE_LEVEL}, logs the provided
+     * message to logcat.
+     *
+     * <p>If {@link #PII_TRACE_LEVEL} is 0, nothing is logged and this method returns immediately.
+     */
+    public void piiTrace(@NonNull String message) {
+        piiTrace(message, /*fastTraceObj=*/null, /*fullTraceObj=*/null);
+    }
+
+    /**
+     * If icing lib interaction tracing is enabled via {@link #PII_TRACE_LEVEL}, logs the provided
+     * message and object to logcat.
+     *
+     * <p>If {@link #PII_TRACE_LEVEL} is 0, nothing is logged and this method returns immediately.
+     * <p>Otherwise, {@code traceObj} is logged if it is non-null.
+     */
+    public void piiTrace(@NonNull String message, @Nullable Object traceObj) {
+        piiTrace(message, /*fastTraceObj=*/traceObj, /*fullTraceObj=*/null);
+    }
+
+    /**
+     * If icing lib interaction tracing is enabled via {@link #PII_TRACE_LEVEL}, logs the provided
+     * message and objects to logcat.
+     *
+     * <p>If {@link #PII_TRACE_LEVEL} is 0, nothing is logged and this method returns immediately.
+     * <p>If {@link #PII_TRACE_LEVEL} is 1, {@code fastTraceObj} is logged if it is non-null.
+     * <p>If {@link #PII_TRACE_LEVEL} is 2, {@code fullTraceObj} is logged if it is non-null, else
+     *   {@code fastTraceObj} is logged if it is non-null..
+     */
+    public void piiTrace(
+            @NonNull String message, @Nullable Object fastTraceObj, @Nullable Object fullTraceObj) {
+        if (PII_TRACE_LEVEL == 0) {
+            return;
+        }
+        StringBuilder builder = new StringBuilder("(trace) ").append(message);
+        if (PII_TRACE_LEVEL == 1 && fastTraceObj != null) {
+            builder.append(": ").append(fastTraceObj);
+        } else if (PII_TRACE_LEVEL == 2 && fullTraceObj != null) {
+            builder.append(": ").append(fullTraceObj);
+        } else if (PII_TRACE_LEVEL == 2 && fastTraceObj != null) {
+            builder.append(": ").append(fastTraceObj);
+        }
+        Log.i(mTag, builder.toString());
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/SchemaMigrationUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/SchemaMigrationUtil.java
new file mode 100644
index 0000000..1408fb8
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/SchemaMigrationUtil.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for schema migration.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SchemaMigrationUtil {
+    private SchemaMigrationUtil() {}
+
+    /**
+     * Returns all active {@link Migrator}s that need to be triggered in this migration.
+     *
+     * <p>{@link Migrator#shouldMigrate} returns {@code true} will make the {@link Migrator} active.
+     */
+    @NonNull
+    public static Map<String, Migrator> getActiveMigrators(
+            @NonNull Set<AppSearchSchema> existingSchemas,
+            @NonNull Map<String, Migrator> migrators,
+            int currentVersion,
+            int finalVersion) {
+        if (currentVersion == finalVersion) {
+            return Collections.emptyMap();
+        }
+        Set<String> existingTypes = new ArraySet<>(existingSchemas.size());
+        for (AppSearchSchema schema : existingSchemas) {
+            existingTypes.add(schema.getSchemaType());
+        }
+
+        Map<String, Migrator> activeMigrators = new ArrayMap<>();
+        for (Map.Entry<String, Migrator> entry : migrators.entrySet()) {
+            // The device contains the source type, and we should trigger migration for the type.
+            String schemaType = entry.getKey();
+            Migrator migrator = entry.getValue();
+            if (existingTypes.contains(schemaType)
+                    && migrator.shouldMigrate(currentVersion, finalVersion)) {
+                activeMigrators.put(schemaType, migrator);
+            }
+        }
+        return activeMigrators;
+    }
+
+    /**
+     * Checks the setSchema() call won't delete any types or has incompatible types after
+     * all {@link Migrator} has been triggered.
+     */
+    public static void checkDeletedAndIncompatibleAfterMigration(
+            @NonNull SetSchemaResponse setSchemaResponse,
+            @NonNull Set<String> activeMigrators) throws AppSearchException {
+        Set<String> unmigratedIncompatibleTypes =
+                new ArraySet<>(setSchemaResponse.getIncompatibleTypes());
+        unmigratedIncompatibleTypes.removeAll(activeMigrators);
+
+        Set<String> unmigratedDeletedTypes =
+                new ArraySet<>(setSchemaResponse.getDeletedTypes());
+        unmigratedDeletedTypes.removeAll(activeMigrators);
+
+        // check if there are any unmigrated incompatible types or deleted types. If there
+        // are, we will getActiveMigratorsthrow an exception. That's the only case we
+        // swallowed in the AppSearchImpl#setSchema().
+        // Since the force override is false, the schema will not have been set if there are
+        // any incompatible or deleted types.
+        checkDeletedAndIncompatible(unmigratedDeletedTypes,
+                unmigratedIncompatibleTypes);
+    }
+
+    /**  Checks the setSchema() call won't delete any types or has incompatible types. */
+    public static void checkDeletedAndIncompatible(@NonNull Set<String> deletedTypes,
+            @NonNull Set<String> incompatibleTypes) throws AppSearchException {
+        if (deletedTypes.size() > 0
+                || incompatibleTypes.size() > 0) {
+            String newMessage = "Schema is incompatible."
+                    + "\n  Deleted types: " + deletedTypes
+                    + "\n  Incompatible types: " + incompatibleTypes;
+            throw new AppSearchException(AppSearchResult.RESULT_INVALID_SCHEMA, newMessage);
+        }
+    }
+}
diff --git a/appsearch/compiler/build.gradle b/appsearch/compiler/build.gradle
index 3ce8ab9..93c22c6 100644
--- a/appsearch/compiler/build.gradle
+++ b/appsearch/compiler/build.gradle
@@ -24,6 +24,10 @@
 
 dependencies {
     api('androidx.annotation:annotation:1.1.0')
+    api(libs.jsr250)
+    implementation(libs.autoCommon)
+    implementation(libs.autoValue)
+    implementation(libs.autoValueAnnotations)
     implementation(libs.javapoet)
 
     // For testing, add in the compiled classes from appsearch to get access to annotations.
@@ -41,6 +45,6 @@
     type = LibraryType.COMPILER_PLUGIN
     mavenGroup = LibraryGroups.APPSEARCH
     inceptionYear = '2019'
-    description = 'Compiler for AndroidX AppSearch data classes'
+    description = 'Compiler for classes annotated with @androidx.appsearch.annotation.Document'
     failOnDeprecationWarnings = false
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
index 63f6e379..90f31aa 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
@@ -15,17 +15,24 @@
  */
 package androidx.appsearch.compiler;
 
+import static javax.lang.model.util.ElementFilter.typesIn;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.google.auto.common.BasicAnnotationProcessor;
+import com.google.auto.common.MoreElements;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.Set;
 
-import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.Messager;
 import javax.annotation.processing.ProcessingEnvironment;
-import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
 import javax.annotation.processing.SupportedOptions;
 import javax.annotation.processing.SupportedSourceVersion;
@@ -33,13 +40,17 @@
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.TypeElement;
-import javax.tools.Diagnostic;
+import javax.tools.Diagnostic.Kind;
 
-/** Processes AppSearchDocument annotations. */
-@SupportedAnnotationTypes({IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS})
+/**
+ * Processes {@code androidx.appsearch.annotation.Document} annotations.
+ *
+ * <p>Only plain Java objects and AutoValue Document classes without builders are supported.
+ */
+@SupportedAnnotationTypes({IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS})
 @SupportedSourceVersion(SourceVersion.RELEASE_8)
 @SupportedOptions({AppSearchCompiler.OUTPUT_DIR_OPTION})
-public class AppSearchCompiler extends AbstractProcessor {
+public class AppSearchCompiler extends BasicAnnotationProcessor {
     /**
      * This property causes us to write output to a different folder instead of the usual filer
      * location. It should only be used for testing.
@@ -47,8 +58,6 @@
     @VisibleForTesting
     static final String OUTPUT_DIR_OPTION = "AppSearchCompiler.OutputDir";
 
-    private Messager mMessager;
-
     @Override
     @NonNull
     public SourceVersion getSupportedSourceVersion() {
@@ -56,73 +65,106 @@
     }
 
     @Override
-    public synchronized void init(@NonNull ProcessingEnvironment processingEnvironment) {
-        super.init(processingEnvironment);
-        mMessager = processingEnvironment.getMessager();
+    protected Iterable<? extends Step> steps() {
+        return ImmutableList.of(new AppSearchCompileStep(processingEnv));
     }
 
-    @Override
-    public boolean process(
-            @NonNull Set<? extends TypeElement> set,
-            @NonNull RoundEnvironment roundEnvironment) {
-        try {
-            tryProcess(set, roundEnvironment);
-        } catch (ProcessingException e) {
-            e.printDiagnostic(mMessager);
+    private static final class AppSearchCompileStep implements Step {
+        private final ProcessingEnvironment mProcessingEnv;
+        private final Messager mMessager;
+
+        AppSearchCompileStep(ProcessingEnvironment processingEnv) {
+            mProcessingEnv = processingEnv;
+            mMessager = processingEnv.getMessager();
         }
-        // True means we claimed the annotations. This is true regardless of whether they were
-        // used correctly.
-        return true;
-    }
 
-    private void tryProcess(
-            @NonNull Set<? extends TypeElement> set,
-            @NonNull RoundEnvironment roundEnvironment) throws ProcessingException {
-        if (set.isEmpty()) return;
+        @Override
+        public ImmutableSet<String> annotations() {
+            return ImmutableSet.of(IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
+        }
 
-        // Find the TypeElement corresponding to the @AppSearchDocument annotation. We can't use the
-        // annotation class directly because the appsearch project compiles only on Android, but
-        // this annotation processor runs on the host.
-        TypeElement appSearchDocument =
-                findAnnotation(set, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+        @Override
+        public ImmutableSet<Element> process(
+                ImmutableSetMultimap<String, Element> elementsByAnnotation) {
+            Set<TypeElement> documentElements =
+                    typesIn(elementsByAnnotation.get(
+                            IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS));
+            for (TypeElement document : documentElements) {
+                try {
+                    processDocument(document);
+                } catch (MissingTypeException e) {
+                    // Save it for next round to wait for the AutoValue annotation processor to
+                    // be run first.
+                    return ImmutableSet.of(e.getTypeName());
+                } catch (ProcessingException e) {
+                    // Prints error message.
+                    e.printDiagnostic(mMessager);
+                }
+            }
+            // No elements will be passed to next round of processing.
+            return ImmutableSet.of();
+        }
 
-        for (Element element : roundEnvironment.getElementsAnnotatedWith(appSearchDocument)) {
+        private void processDocument(@NonNull TypeElement element)
+                throws ProcessingException, MissingTypeException {
             if (element.getKind() != ElementKind.CLASS) {
                 throw new ProcessingException(
-                        "@AppSearchDocument annotation on something other than a class", element);
+                        "@Document annotation on something other than a class", element);
             }
-            processAppSearchDocument((TypeElement) element);
-        }
-    }
 
-    private void processAppSearchDocument(@NonNull TypeElement element) throws ProcessingException {
-        AppSearchDocumentModel model = AppSearchDocumentModel.create(processingEnv, element);
-        CodeGenerator generator = CodeGenerator.generate(processingEnv, model);
-        String outputDir = processingEnv.getOptions().get(OUTPUT_DIR_OPTION);
-        try {
-            if (outputDir == null || outputDir.isEmpty()) {
-                generator.writeToFiler();
+            DocumentModel model;
+            if (element.getAnnotation(AutoValue.class) != null) {
+                // Document class is annotated as AutoValue class. For processing the AutoValue
+                // class, we also need the generated class from AutoValue annotation processor.
+                TypeElement generatedElement =
+                        mProcessingEnv.getElementUtils().getTypeElement(
+                                getAutoValueGeneratedClassName(element));
+                if (generatedElement == null) {
+                    // Generated class is not found.
+                    throw new MissingTypeException(element);
+                } else {
+                    model = DocumentModel.createAutoValueModel(mProcessingEnv, element,
+                            generatedElement);
+                }
             } else {
-                mMessager.printMessage(
-                        Diagnostic.Kind.NOTE,
-                        "Writing output to \"" + outputDir
-                                + "\" due to the presence of -A" + OUTPUT_DIR_OPTION);
-                generator.writeToFolder(new File(outputDir));
+                // Non-AutoValue AppSearch Document class.
+                model = DocumentModel.createPojoModel(mProcessingEnv, element);
             }
-        } catch (IOException e) {
-            ProcessingException pe =
-                    new ProcessingException("Failed to write output", model.getClassElement());
-            pe.initCause(e);
-            throw pe;
+            CodeGenerator generator = CodeGenerator.generate(mProcessingEnv, model);
+            String outputDir = mProcessingEnv.getOptions().get(OUTPUT_DIR_OPTION);
+            try {
+                if (outputDir == null || outputDir.isEmpty()) {
+                    generator.writeToFiler();
+                } else {
+                    mMessager.printMessage(
+                            Kind.NOTE,
+                            "Writing output to \"" + outputDir
+                                    + "\" due to the presence of -A" + OUTPUT_DIR_OPTION);
+                    generator.writeToFolder(new File(outputDir));
+                }
+            } catch (IOException e) {
+                ProcessingException pe =
+                        new ProcessingException("Failed to write output", model.getClassElement());
+                pe.initCause(e);
+                throw pe;
+            }
         }
-    }
 
-    private TypeElement findAnnotation(Set<? extends TypeElement> set, String name) {
-        for (TypeElement typeElement : set) {
-            if (typeElement.getQualifiedName().contentEquals(name)) {
-                return typeElement;
+        /**
+         * Gets the generated class name of an AutoValue annotated class.
+         *
+         * <p>This is the same naming strategy used by AutoValue's processor.
+         */
+        private String getAutoValueGeneratedClassName(TypeElement element) {
+            TypeElement type = element;
+            String name = type.getSimpleName().toString();
+            while (type.getEnclosingElement() instanceof TypeElement) {
+                type = (TypeElement) type.getEnclosingElement();
+                name = type.getSimpleName().toString() + "_" + name;
             }
+            String pkg = MoreElements.getPackage(type).getQualifiedName().toString();
+            String dot = pkg.isEmpty() ? "" : ".";
+            return pkg + dot + "AutoValue_" + name;
         }
-        return null;
     }
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java
deleted file mode 100644
index 76578db..0000000
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java
+++ /dev/null
@@ -1,442 +0,0 @@
-/*
- * Copyright 2018 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 androidx.appsearch.compiler;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.processing.ProcessingEnvironment;
-import javax.lang.model.element.AnnotationMirror;
-import javax.lang.model.element.Element;
-import javax.lang.model.element.ElementKind;
-import javax.lang.model.element.ExecutableElement;
-import javax.lang.model.element.Modifier;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.element.VariableElement;
-
-/**
- * Processes AppSearchDocument annotations.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class AppSearchDocumentModel {
-
-    /** Enumeration of fields that must be handled specially (i.e. are not properties) */
-    enum SpecialField { URI, NAMESPACE, CREATION_TIMESTAMP_MILLIS, TTL_MILLIS, SCORE }
-    /** Determines how the annotation processor has decided to read the value of a field. */
-    enum ReadKind { FIELD, GETTER }
-    /** Determines how the annotation processor has decided to write the value of a field. */
-    enum WriteKind { FIELD, SETTER, CONSTRUCTOR }
-
-    private final IntrospectionHelper mIntrospectionHelper;
-    private final TypeElement mClass;
-    private final AnnotationMirror mAppSearchDocumentAnnotation;
-    private final Set<ExecutableElement> mConstructors = new LinkedHashSet<>();
-    private final Set<ExecutableElement> mMethods = new LinkedHashSet<>();
-    private final Map<String, VariableElement> mAllAppSearchFields = new LinkedHashMap<>();
-    private final Map<String, VariableElement> mPropertyFields = new LinkedHashMap<>();
-    private final Map<SpecialField, String> mSpecialFieldNames = new EnumMap<>(SpecialField.class);
-    private final Map<VariableElement, ReadKind> mReadKinds = new HashMap<>();
-    private final Map<VariableElement, WriteKind> mWriteKinds = new HashMap<>();
-    private final Map<VariableElement, ProcessingException> mWriteWhyConstructor = new HashMap<>();
-    private List<String> mChosenConstructorParams = null;
-
-    private AppSearchDocumentModel(
-            @NonNull ProcessingEnvironment env,
-            @NonNull TypeElement clazz)
-            throws ProcessingException {
-        mIntrospectionHelper = new IntrospectionHelper(env);
-        mClass = clazz;
-        if (mClass.getModifiers().contains(Modifier.PRIVATE)) {
-            throw new ProcessingException("@AppSearchDocument annotated class is private", mClass);
-        }
-
-        mAppSearchDocumentAnnotation = mIntrospectionHelper.getAnnotation(
-                mClass, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
-
-        // Scan methods and constructors. AppSearch doesn't define any annotations that apply to
-        // these, but we will need this info when processing fields to make sure the fields can
-        // be get and set.
-        for (Element child : mClass.getEnclosedElements()) {
-            if (child.getKind() == ElementKind.CONSTRUCTOR) {
-                mConstructors.add((ExecutableElement) child);
-            } else if (child.getKind() == ElementKind.METHOD) {
-                mMethods.add((ExecutableElement) child);
-            }
-        }
-
-        scanFields();
-        scanConstructors();
-    }
-
-    @NonNull
-    public TypeElement getClassElement() {
-        return mClass;
-    }
-
-    @NonNull
-    public String getSchemaName() {
-        Map<String, Object> params =
-                mIntrospectionHelper.getAnnotationParams(mAppSearchDocumentAnnotation);
-        String name = params.get("name").toString();
-        if (name.isEmpty()) {
-            return mClass.getSimpleName().toString();
-        }
-        return name;
-    }
-
-    @NonNull
-    public Map<String, VariableElement> getAllFields() {
-        return Collections.unmodifiableMap(mAllAppSearchFields);
-    }
-
-    @NonNull
-    public Map<String, VariableElement> getPropertyFields() {
-        return Collections.unmodifiableMap(mPropertyFields);
-    }
-
-    @Nullable
-    public String getSpecialFieldName(SpecialField field) {
-        return mSpecialFieldNames.get(field);
-    }
-
-    @Nullable
-    public ReadKind getFieldReadKind(String fieldName) {
-        VariableElement element = mAllAppSearchFields.get(fieldName);
-        return mReadKinds.get(element);
-    }
-
-    @Nullable
-    public WriteKind getFieldWriteKind(String fieldName) {
-        VariableElement element = mAllAppSearchFields.get(fieldName);
-        return mWriteKinds.get(element);
-    }
-
-    /**
-     * Finds the AppSearch name for the given property.
-     *
-     * <p>This is usually the name of the field in Java, but may be changed if the developer
-     * specifies a different 'name' parameter in the annotation.
-     */
-    @NonNull
-    public String getPropertyName(@NonNull VariableElement property) throws ProcessingException {
-        AnnotationMirror annotation =
-                mIntrospectionHelper.getAnnotation(property, IntrospectionHelper.PROPERTY_CLASS);
-        Map<String, Object> params = mIntrospectionHelper.getAnnotationParams(annotation);
-        String propertyName = params.get("name").toString();
-        if (propertyName.isEmpty()) {
-            propertyName = property.getSimpleName().toString();
-        }
-        return propertyName;
-    }
-
-    @NonNull
-    public List<String> getChosenConstructorParams() {
-        return Collections.unmodifiableList(mChosenConstructorParams);
-    }
-
-    private void scanFields() throws ProcessingException {
-        Element uriField = null;
-        Element namespaceField = null;
-        Element creationTimestampField = null;
-        Element ttlField = null;
-        Element scoreField = null;
-        for (Element childElement : mClass.getEnclosedElements()) {
-            if (!childElement.getKind().isField()) continue;
-            VariableElement child = (VariableElement) childElement;
-            String fieldName = child.getSimpleName().toString();
-            for (AnnotationMirror annotation : child.getAnnotationMirrors()) {
-                String annotationFq = annotation.getAnnotationType().toString();
-                boolean isAppSearchField = true;
-                if (IntrospectionHelper.URI_CLASS.equals(annotationFq)) {
-                    if (uriField != null) {
-                        throw new ProcessingException(
-                                "Class contains multiple fields annotated @Uri", child);
-                    }
-                    uriField = child;
-                    mSpecialFieldNames.put(SpecialField.URI, fieldName);
-
-                } else if (IntrospectionHelper.NAMESPACE_CLASS.equals(annotationFq)) {
-                    if (namespaceField != null) {
-                        throw new ProcessingException(
-                                "Class contains multiple fields annotated @Namespace", child);
-                    }
-                    namespaceField = child;
-                    mSpecialFieldNames.put(SpecialField.NAMESPACE, fieldName);
-
-                } else if (
-                        IntrospectionHelper.CREATION_TIMESTAMP_MILLIS_CLASS.equals(annotationFq)) {
-                    if (creationTimestampField != null) {
-                        throw new ProcessingException(
-                                "Class contains multiple fields annotated @CreationTimestampMillis",
-                                child);
-                    }
-                    creationTimestampField = child;
-                    mSpecialFieldNames.put(SpecialField.CREATION_TIMESTAMP_MILLIS, fieldName);
-
-                } else if (IntrospectionHelper.TTL_MILLIS_CLASS.equals(annotationFq)) {
-                    if (ttlField != null) {
-                        throw new ProcessingException(
-                                "Class contains multiple fields annotated @TtlMillis", child);
-                    }
-                    ttlField = child;
-                    mSpecialFieldNames.put(SpecialField.TTL_MILLIS, fieldName);
-
-                } else if (IntrospectionHelper.SCORE_CLASS.equals(annotationFq)) {
-                    if (scoreField != null) {
-                        throw new ProcessingException(
-                                "Class contains multiple fields annotated @Score", child);
-                    }
-                    scoreField = child;
-                    mSpecialFieldNames.put(SpecialField.SCORE, fieldName);
-
-                } else if (IntrospectionHelper.PROPERTY_CLASS.equals(annotationFq)) {
-                    mPropertyFields.put(fieldName, child);
-
-                } else {
-                    isAppSearchField = false;
-                }
-
-                if (isAppSearchField) {
-                    mAllAppSearchFields.put(fieldName, child);
-                }
-            }
-        }
-
-        // Every document must always have a URI
-        if (uriField == null) {
-            throw new ProcessingException(
-                    "All @AppSearchDocument classes must have exactly one field annotated with "
-                            + "@Uri", mClass);
-        }
-
-        for (VariableElement appSearchField : mAllAppSearchFields.values()) {
-            chooseAccessKinds(appSearchField);
-        }
-    }
-
-    /**
-     * Chooses how to access the given field for read and write, subject to our requirements for all
-     * AppSearch-managed class fields:
-     *
-     * <p>For read: visible field, or visible getter
-     * <p>For write: visible mutable field, or visible setter, or visible constructor accepting at
-     *   minimum all fields that aren't mutable and have no visible setter.
-     *
-     * @throws ProcessingException if no access type is possible for the given field
-     */
-    private void chooseAccessKinds(@NonNull VariableElement field)
-            throws ProcessingException {
-        // Choose get access
-        String fieldName = field.getSimpleName().toString();
-        Set<Modifier> modifiers = field.getModifiers();
-        if (modifiers.contains(Modifier.PRIVATE)) {
-            String getterName = getAccessorName(fieldName, /*get=*/ true);
-            findGetter(field, getterName);
-            mReadKinds.put(field, ReadKind.GETTER);
-        } else {
-            mReadKinds.put(field, ReadKind.FIELD);
-        }
-
-        // Choose set access
-        if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.FINAL)
-                || modifiers.contains(Modifier.STATIC)) {
-            // Try to find a setter. If we can't find one, mark the WriteKind as CONSTRUCTOR. We
-            // don't know if this is true yet, the constructors will be inspected in a subsequent
-            // pass.
-            String setterName = getAccessorName(fieldName, /*get=*/ false);
-            try {
-                findSetter(field, setterName);
-                mWriteKinds.put(field, WriteKind.SETTER);
-            } catch (ProcessingException e) {
-                // We'll look for a constructor, so we may still be able to set this field,
-                // but it's more likely the developer configured the setter incorrectly. Keep
-                // the exception around to include it in the report if no constructor is found.
-                mWriteWhyConstructor.put(field, e);
-                mWriteKinds.put(field, WriteKind.CONSTRUCTOR);
-            }
-        } else {
-            mWriteKinds.put(field, WriteKind.FIELD);
-        }
-    }
-
-    private void findGetter(@NonNull VariableElement field, @NonNull String getterName)
-            throws ProcessingException {
-        ProcessingException e = new ProcessingException(
-                "Field cannot be read: it is private and we failed to find a suitable getter named "
-                        + "\"" + getterName + "\"",
-                field);
-
-        for (ExecutableElement method : mMethods) {
-            if (!method.getSimpleName().toString().equals(getterName)) {
-                continue;
-            }
-            if (method.getModifiers().contains(Modifier.PRIVATE)) {
-                e.addWarning(new ProcessingException(
-                        "Getter cannot be used: private visibility", method));
-                continue;
-            }
-            if (!method.getParameters().isEmpty()) {
-                e.addWarning(new ProcessingException(
-                        "Getter cannot be used: should take no parameters", method));
-                continue;
-            }
-            // Found one!
-            return;
-        }
-
-        // Broke out of the loop without finding anything.
-        throw e;
-    }
-
-    private void findSetter(@NonNull VariableElement field, @NonNull String setterName)
-            throws ProcessingException {
-        // We can't report setter failure until we've searched the constructors, so this message is
-        // anticipatory and should be buffered by the caller.
-        ProcessingException e = new ProcessingException(
-                "Field cannot be written directly or via setter because it is private, final, or "
-                        + "static, and we failed to find a suitable setter named \""
-                        + setterName + "\". Trying to find a suitable constructor.",
-                field);
-
-        for (ExecutableElement method : mMethods) {
-            if (!method.getSimpleName().toString().equals(setterName)) {
-                continue;
-            }
-            if (method.getModifiers().contains(Modifier.PRIVATE)) {
-                e.addWarning(new ProcessingException(
-                        "Setter cannot be used: private visibility", method));
-                continue;
-            }
-            if (method.getParameters().size() != 1) {
-                e.addWarning(new ProcessingException(
-                        "Setter cannot be used: takes " + method.getParameters().size()
-                                + " parameters instead of 1",
-                        method));
-                continue;
-            }
-            // Found one!
-            return;
-        }
-
-        // Broke out of the loop without finding anything.
-        throw e;
-    }
-
-    private void scanConstructors() throws ProcessingException {
-        // Maps name to Element
-        Map<String, VariableElement> constructorWrittenFields = new LinkedHashMap<>();
-        for (Map.Entry<VariableElement, WriteKind> it : mWriteKinds.entrySet()) {
-            if (it.getValue() == WriteKind.CONSTRUCTOR) {
-                String name = it.getKey().getSimpleName().toString();
-                constructorWrittenFields.put(name, it.getKey());
-            }
-        }
-
-        Map<ExecutableElement, String> whyNotConstructor = new HashMap<>();
-        constructorSearch: for (ExecutableElement constructor : mConstructors) {
-            if (constructor.getModifiers().contains(Modifier.PRIVATE)) {
-                whyNotConstructor.put(constructor, "Constructor is private");
-                continue constructorSearch;
-            }
-            List<String> constructorParamFields = new ArrayList<>();
-            Set<String> remainingFields = new HashSet<>(constructorWrittenFields.keySet());
-            for (VariableElement parameter : constructor.getParameters()) {
-                String name = parameter.getSimpleName().toString();
-                if (!mAllAppSearchFields.containsKey(name)) {
-                    whyNotConstructor.put(
-                            constructor,
-                            "Parameter \"" + name + "\" is not an AppSearch parameter; don't know "
-                                    + "how to supply it.");
-                    continue constructorSearch;
-                }
-                remainingFields.remove(name);
-                constructorParamFields.add(name);
-            }
-            if (!remainingFields.isEmpty()) {
-                whyNotConstructor.put(
-                        constructor,
-                        "This constructor doesn't have parameters for the following fields: "
-                                + remainingFields);
-                continue constructorSearch;
-            }
-            // Found one!
-            mChosenConstructorParams = constructorParamFields;
-            return;
-        }
-
-        // If we got here, we couldn't find any constructors.
-        ProcessingException e =
-                new ProcessingException(
-                        "Failed to find any suitable constructors to build this class. See "
-                                + "warnings for details.", mClass);
-
-        // Inform the developer why we started looking for constructors in the first place
-        for (VariableElement field : constructorWrittenFields.values()) {
-            ProcessingException warning = mWriteWhyConstructor.get(field);
-            if (warning != null) {
-                e.addWarning(warning);
-            }
-        }
-
-        // Inform the developer about why each constructor we considered was rejected
-        for (Map.Entry<ExecutableElement, String> it : whyNotConstructor.entrySet()) {
-            ProcessingException warning = new ProcessingException(
-                    "Cannot use this constructor to construct the class: " + it.getValue(),
-                    it.getKey());
-            e.addWarning(warning);
-        }
-
-        throw e;
-    }
-
-    public String getAccessorName(String fieldName, boolean get) {
-        char fieldNameFirst = fieldName.charAt(0);
-        StringBuilder methodNameBuilder = new StringBuilder();
-        methodNameBuilder.append(Character.toUpperCase(fieldNameFirst));
-        if (fieldName.length() > 1) {
-            methodNameBuilder.append(fieldName.subSequence(1, fieldName.length()));
-        }
-        if (get) {
-            return "get" + methodNameBuilder;
-        } else {
-            return "set" + methodNameBuilder;
-        }
-    }
-
-    /**
-     * Tries to create an {@link AppSearchDocumentModel} from the given {@link Element}.
-     *
-     * @throws ProcessingException if the @{@code AppSearchDocument}-annotated class is invalid.
-     */
-    public static AppSearchDocumentModel create(
-            @NonNull ProcessingEnvironment env, @NonNull TypeElement clazz)
-            throws ProcessingException {
-        return new AppSearchDocumentModel(env, clazz);
-    }
-}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
index 9ee924a..4b8aa80 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
@@ -17,8 +17,9 @@
 package androidx.appsearch.compiler;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.JavaFile;
 import com.squareup.javapoet.ParameterizedTypeName;
 import com.squareup.javapoet.TypeName;
@@ -27,32 +28,30 @@
 import java.io.File;
 import java.io.IOException;
 
+import javax.annotation.Generated;
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.Modifier;
 
 /**
  * Generates java code for an {@link androidx.appsearch.app.AppSearchSchema} and a translator
- * between the data class and a {@link androidx.appsearch.app.GenericDocument}.
+ * between the document class and a {@link androidx.appsearch.app.GenericDocument}.
  */
 class CodeGenerator {
-    @VisibleForTesting
-    static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
-
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
+    private final DocumentModel mModel;
 
     private final String mOutputPackage;
     private final TypeSpec mOutputClass;
 
     public static CodeGenerator generate(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model)
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model)
             throws ProcessingException {
         return new CodeGenerator(env, model);
     }
 
     private CodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model)
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model)
             throws ProcessingException {
         // Prepare constants needed for processing
         mEnv = env;
@@ -74,27 +73,23 @@
 
     /**
      * Creates factory class for any class annotated with
-     * {@link androidx.appsearch.annotation.AppSearchDocument}
+     * {@link androidx.appsearch.annotation.Document}
      * <p>Class Example 1:
-     *   For a class Foo annotated with @AppSearchDocument, we will generated a
+     *   For a class Foo annotated with @Document, we will generated a
      *   $$__AppSearch__Foo.class under the output package.
      * <p>Class Example 2:
-     *   For an inner class Foo.Bar annotated with @AppSearchDocument, we will generated a
+     *   For an inner class Foo.Bar annotated with @Document, we will generated a
      *   $$__AppSearch__Foo$$__Bar.class under the output package.
      */
     private TypeSpec createClass() throws ProcessingException {
         // Gets the full name of target class.
         String qualifiedName = mModel.getClassElement().getQualifiedName().toString();
-        String packageName = mOutputPackage + ".";
-
-        // Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
-        // for inner class Foo.Bar.
-        String genClassName = GEN_CLASS_PREFIX
-                + qualifiedName.substring(packageName.length()).replace(".", "$$__");
+        String className = qualifiedName.substring(mOutputPackage.length() + 1);
+        ClassName genClassName = mHelper.getDocumentClassFactoryForClass(mOutputPackage, className);
 
         TypeName genClassType = TypeName.get(mModel.getClassElement().asType());
         TypeName factoryType = ParameterizedTypeName.get(
-                mHelper.getAppSearchClass("DataClassFactory"),
+                mHelper.getAppSearchClass("DocumentClassFactory"),
                 genClassType);
 
         TypeSpec.Builder genClass = TypeSpec
@@ -103,6 +98,12 @@
                 .addModifiers(Modifier.PUBLIC)
                 .addSuperinterface(factoryType);
 
+        // Add the @Generated annotation to avoid static analysis running on these files
+        genClass.addAnnotation(
+                AnnotationSpec.builder(Generated.class)
+                        .addMember("value", "$S", AppSearchCompiler.class.getCanonicalName())
+                        .build());
+
         SchemaCodeGenerator.generate(mEnv, mModel, genClass);
         ToGenericDocumentCodeGenerator.generate(mEnv, mModel, genClass);
         FromGenericDocumentCodeGenerator.generate(mEnv, mModel, genClass);
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
new file mode 100644
index 0000000..a93a6f1
--- /dev/null
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
@@ -0,0 +1,700 @@
+/*
+ * Copyright 2018 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 androidx.appsearch.compiler;
+
+import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.compiler.IntrospectionHelper.PropertyClass;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.util.ElementFilter;
+
+/**
+ * Processes @Document annotations.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class DocumentModel {
+
+    /** Enumeration of fields that must be handled specially (i.e. are not properties) */
+    enum SpecialField {ID, NAMESPACE, CREATION_TIMESTAMP_MILLIS, TTL_MILLIS, SCORE}
+
+    /** Determines how the annotation processor has decided to read the value of a field. */
+    enum ReadKind {FIELD, GETTER}
+
+    /** Determines how the annotation processor has decided to write the value of a field. */
+    enum WriteKind {FIELD, SETTER, CREATION_METHOD}
+
+    private final IntrospectionHelper mHelper;
+    private final TypeElement mClass;
+    private final AnnotationMirror mDocumentAnnotation;
+    // Warning: if you change this to a HashSet, we may choose different getters or setters from
+    // run to run, causing the generated code to bounce.
+    private final Set<ExecutableElement> mAllMethods = new LinkedHashSet<>();
+    private final boolean mIsAutoValueDocument;
+    // Key: Name of the field which is accessed through the getter method.
+    // Value: ExecutableElement of the getter method.
+    private final Map<String, ExecutableElement> mGetterMethods = new HashMap<>();
+    // Key: Name of the field whose value is set through the setter method.
+    // Value: ExecutableElement of the setter method.
+    private final Map<String, ExecutableElement> mSetterMethods = new HashMap<>();
+    // Warning: if you change this to a HashMap, we may assign fields in a different order from run
+    // to run, causing the generated code to bounce.
+    private final Map<String, VariableElement> mAllAppSearchFields = new LinkedHashMap<>();
+    // Warning: if you change this to a HashMap, we may assign fields in a different order from run
+    // to run, causing the generated code to bounce.
+    private final Map<String, VariableElement> mPropertyFields = new LinkedHashMap<>();
+    private final Map<SpecialField, String> mSpecialFieldNames = new EnumMap<>(SpecialField.class);
+    private final Map<VariableElement, ReadKind> mReadKinds = new HashMap<>();
+    private final Map<VariableElement, WriteKind> mWriteKinds = new HashMap<>();
+    // Contains the reason why that field couldn't be written either by field or by setter.
+    private final Map<VariableElement, ProcessingException> mWriteWhyCreationMethod =
+            new HashMap<>();
+    private ExecutableElement mChosenCreationMethod = null;
+    private List<String> mChosenCreationMethodParams = null;
+
+    private DocumentModel(
+            @NonNull ProcessingEnvironment env,
+            @NonNull TypeElement clazz,
+            @Nullable TypeElement generatedAutoValueElement)
+            throws ProcessingException {
+        if (clazz.getModifiers().contains(Modifier.PRIVATE)) {
+            throw new ProcessingException("@Document annotated class is private", clazz);
+        }
+
+        mHelper = new IntrospectionHelper(env);
+        mClass = clazz;
+        mDocumentAnnotation = getDocumentAnnotation(mClass);
+
+        if (generatedAutoValueElement != null) {
+            mIsAutoValueDocument = true;
+            // Scan factory methods from AutoValue class.
+            Set<ExecutableElement> creationMethods = new LinkedHashSet<>();
+            for (Element child : ElementFilter.methodsIn(mClass.getEnclosedElements())) {
+                ExecutableElement method = (ExecutableElement) child;
+                if (isFactoryMethod(method)) {
+                    creationMethods.add(method);
+                }
+            }
+            mAllMethods.addAll(
+                    ElementFilter.methodsIn(generatedAutoValueElement.getEnclosedElements()));
+
+            scanFields(generatedAutoValueElement);
+            scanCreationMethods(creationMethods);
+        } else {
+            mIsAutoValueDocument = false;
+            // Scan methods and constructors. We will need this info when processing fields to
+            // make sure the fields can be get and set.
+            Set<ExecutableElement> creationMethods = new LinkedHashSet<>();
+            for (Element child : mClass.getEnclosedElements()) {
+                if (child.getKind() == ElementKind.CONSTRUCTOR) {
+                    creationMethods.add((ExecutableElement) child);
+                } else if (child.getKind() == ElementKind.METHOD) {
+                    ExecutableElement method = (ExecutableElement) child;
+                    mAllMethods.add(method);
+                    if (isFactoryMethod(method)) {
+                        creationMethods.add(method);
+                    }
+                }
+            }
+
+            scanFields(mClass);
+            scanCreationMethods(creationMethods);
+        }
+    }
+
+    /**
+     * Tries to create an {@link DocumentModel} from the given {@link Element}.
+     *
+     * @throws ProcessingException if the @{@code Document}-annotated class is invalid.
+     */
+    public static DocumentModel createPojoModel(
+            @NonNull ProcessingEnvironment env, @NonNull TypeElement clazz)
+            throws ProcessingException {
+        return new DocumentModel(env, clazz, null);
+    }
+
+    /**
+     * Tries to create an {@link DocumentModel} from the given AutoValue {@link Element} and
+     * corresponding generated class.
+     *
+     * @throws ProcessingException if the @{@code Document}-annotated class is invalid.
+     */
+    public static DocumentModel createAutoValueModel(
+            @NonNull ProcessingEnvironment env, @NonNull TypeElement clazz,
+            @NonNull TypeElement generatedAutoValueElement)
+            throws ProcessingException {
+        return new DocumentModel(env, clazz, generatedAutoValueElement);
+    }
+
+    @NonNull
+    public TypeElement getClassElement() {
+        return mClass;
+    }
+
+    @NonNull
+    public String getSchemaName() {
+        Map<String, Object> params =
+                mHelper.getAnnotationParams(mDocumentAnnotation);
+        String name = params.get("name").toString();
+        if (name.isEmpty()) {
+            return mClass.getSimpleName().toString();
+        }
+        return name;
+    }
+
+    @NonNull
+    public Map<String, VariableElement> getAllFields() {
+        return Collections.unmodifiableMap(mAllAppSearchFields);
+    }
+
+    @NonNull
+    public Map<String, VariableElement> getPropertyFields() {
+        return Collections.unmodifiableMap(mPropertyFields);
+    }
+
+    @Nullable
+    public String getSpecialFieldName(SpecialField field) {
+        return mSpecialFieldNames.get(field);
+    }
+
+    @Nullable
+    public ReadKind getFieldReadKind(String fieldName) {
+        VariableElement element = mAllAppSearchFields.get(fieldName);
+        return mReadKinds.get(element);
+    }
+
+    @Nullable
+    public WriteKind getFieldWriteKind(String fieldName) {
+        VariableElement element = mAllAppSearchFields.get(fieldName);
+        return mWriteKinds.get(element);
+    }
+
+    @Nullable
+    public ExecutableElement getGetterForField(String fieldName) {
+        return mGetterMethods.get(fieldName);
+    }
+
+    @Nullable
+    public ExecutableElement getSetterForField(String fieldName) {
+        return mSetterMethods.get(fieldName);
+    }
+
+    /**
+     * Finds the AppSearch name for the given property.
+     *
+     * <p>This is usually the name of the field in Java, but may be changed if the developer
+     * specifies a different 'name' parameter in the annotation.
+     */
+    @NonNull
+    public String getPropertyName(@NonNull VariableElement property) throws ProcessingException {
+        AnnotationMirror annotation = getPropertyAnnotation(property);
+        Map<String, Object> params = mHelper.getAnnotationParams(annotation);
+        String propertyName = params.get("name").toString();
+        if (propertyName.isEmpty()) {
+            propertyName = getNormalizedFieldName(property.getSimpleName().toString());
+        }
+        return propertyName;
+    }
+
+    /**
+     * Returns the first found AppSearch property annotation element from the input element's
+     * annotations.
+     *
+     * @throws ProcessingException if no AppSearch property annotation is found.
+     */
+    @NonNull
+    public AnnotationMirror getPropertyAnnotation(@NonNull Element element)
+            throws ProcessingException {
+        Objects.requireNonNull(element);
+        if (mIsAutoValueDocument) {
+            element = getGetterForField(element.getSimpleName().toString());
+        }
+        Set<String> propertyClassPaths = new HashSet<>();
+        for (PropertyClass propertyClass : PropertyClass.values()) {
+            propertyClassPaths.add(propertyClass.getClassFullPath());
+        }
+        for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
+            String annotationFq = annotation.getAnnotationType().toString();
+            if (propertyClassPaths.contains(annotationFq)) {
+                return annotation;
+            }
+        }
+        throw new ProcessingException("Missing AppSearch property annotation.", element);
+    }
+
+    @NonNull
+    public ExecutableElement getChosenCreationMethod() {
+        return mChosenCreationMethod;
+    }
+
+    @NonNull
+    public List<String> getChosenCreationMethodParams() {
+        return Collections.unmodifiableList(mChosenCreationMethodParams);
+    }
+
+    private boolean isFactoryMethod(ExecutableElement method) {
+        Set<Modifier> methodModifiers = method.getModifiers();
+        return methodModifiers.contains(Modifier.STATIC)
+                && !methodModifiers.contains(Modifier.PRIVATE)
+                && method.getReturnType() == mClass.asType();
+    }
+
+    private void scanFields(TypeElement element) throws ProcessingException {
+        Element namespaceField = null;
+        Element idField = null;
+        Element creationTimestampField = null;
+        Element ttlField = null;
+        Element scoreField = null;
+        List<? extends Element> enclosedElements = element.getEnclosedElements();
+        for (int i = 0; i < enclosedElements.size(); i++) {
+            Element childElement = enclosedElements.get(i);
+            if (mIsAutoValueDocument && childElement.getKind() != ElementKind.METHOD) {
+                continue;
+            }
+            String fieldName = childElement.getSimpleName().toString();
+            for (AnnotationMirror annotation : childElement.getAnnotationMirrors()) {
+                String annotationFq = annotation.getAnnotationType().toString();
+                if (!annotationFq.startsWith(DOCUMENT_ANNOTATION_CLASS)) {
+                    continue;
+                }
+                VariableElement child;
+                if (mIsAutoValueDocument) {
+                    child = findFieldForFunctionWithSameName(enclosedElements, childElement);
+                } else {
+                    if (childElement.getKind() == ElementKind.METHOD) {
+                        throw new ProcessingException(
+                                "AppSearch annotation is not applicable to methods for "
+                                        + "Non-AutoValue class",
+                                childElement);
+                    } else {
+                        child = (VariableElement) childElement;
+                    }
+                }
+                switch (annotationFq) {
+                    case IntrospectionHelper.ID_CLASS:
+                        if (idField != null) {
+                            throw new ProcessingException(
+                                    "Class contains multiple fields annotated @Id", child);
+                        }
+                        idField = child;
+                        mSpecialFieldNames.put(SpecialField.ID, fieldName);
+                        break;
+                    case IntrospectionHelper.NAMESPACE_CLASS:
+                        if (namespaceField != null) {
+                            throw new ProcessingException(
+                                    "Class contains multiple fields annotated @Namespace",
+                                    child);
+                        }
+                        namespaceField = child;
+                        mSpecialFieldNames.put(SpecialField.NAMESPACE, fieldName);
+                        break;
+                    case IntrospectionHelper.CREATION_TIMESTAMP_MILLIS_CLASS:
+                        if (creationTimestampField != null) {
+                            throw new ProcessingException(
+                                    "Class contains multiple fields annotated "
+                                            + "@CreationTimestampMillis",
+                                    child);
+                        }
+                        creationTimestampField = child;
+                        mSpecialFieldNames.put(SpecialField.CREATION_TIMESTAMP_MILLIS, fieldName);
+                        break;
+                    case IntrospectionHelper.TTL_MILLIS_CLASS:
+                        if (ttlField != null) {
+                            throw new ProcessingException(
+                                    "Class contains multiple fields annotated @TtlMillis",
+                                    child);
+                        }
+                        ttlField = child;
+                        mSpecialFieldNames.put(SpecialField.TTL_MILLIS, fieldName);
+                        break;
+                    case IntrospectionHelper.SCORE_CLASS:
+                        if (scoreField != null) {
+                            throw new ProcessingException(
+                                    "Class contains multiple fields annotated @Score", child);
+                        }
+                        scoreField = child;
+                        mSpecialFieldNames.put(SpecialField.SCORE, fieldName);
+                        break;
+                    default:
+                        PropertyClass propertyClass = getPropertyClass(annotationFq);
+                        if (propertyClass != null) {
+                            checkFieldTypeForPropertyAnnotation(child, propertyClass);
+                            mPropertyFields.put(fieldName, child);
+                        }
+                }
+                mAllAppSearchFields.put(fieldName, child);
+            }
+        }
+
+        // Every document must always have a namespace
+        if (namespaceField == null) {
+            throw new ProcessingException(
+                    "All @Document classes must have exactly one field annotated with @Namespace",
+                    mClass);
+        }
+
+        // Every document must always have an ID
+        if (idField == null) {
+            throw new ProcessingException(
+                    "All @Document classes must have exactly one field annotated with @Id",
+                    mClass);
+        }
+
+        for (VariableElement appSearchField : mAllAppSearchFields.values()) {
+            chooseAccessKinds(appSearchField);
+        }
+    }
+
+    @NonNull
+    private VariableElement findFieldForFunctionWithSameName(
+            @NonNull List<? extends Element> elements,
+            @NonNull Element functionElement) throws ProcessingException {
+        String fieldName = functionElement.getSimpleName().toString();
+        for (VariableElement field : ElementFilter.fieldsIn(elements)) {
+            if (fieldName.equals(field.getSimpleName().toString())) {
+                return field;
+            }
+        }
+        throw new ProcessingException(
+                "Cannot find the corresponding field for the annotated function",
+                functionElement);
+    }
+
+    /**
+     * Checks whether property's data type matches the {@code androidx.appsearch.annotation
+     * .Document} property annotation's requirement.
+     *
+     * @throws ProcessingException if data type doesn't match property annotation's requirement.
+     */
+    void checkFieldTypeForPropertyAnnotation(@NonNull VariableElement property,
+            PropertyClass propertyClass) throws ProcessingException {
+        switch (propertyClass) {
+            case BOOLEAN_PROPERTY_CLASS:
+                if (mHelper.isFieldOfExactType(property, mHelper.mBooleanBoxType,
+                        mHelper.mBooleanPrimitiveType)) {
+                    return;
+                }
+                break;
+            case BYTES_PROPERTY_CLASS:
+                if (mHelper.isFieldOfExactType(property, mHelper.mByteBoxType,
+                        mHelper.mBytePrimitiveType, mHelper.mByteBoxArrayType,
+                        mHelper.mBytePrimitiveArrayType)) {
+                    return;
+                }
+                break;
+            case DOCUMENT_PROPERTY_CLASS:
+                if (mHelper.isFieldOfDocumentType(property)) {
+                    return;
+                }
+                break;
+            case DOUBLE_PROPERTY_CLASS:
+                if (mHelper.isFieldOfExactType(property, mHelper.mDoubleBoxType,
+                        mHelper.mDoublePrimitiveType, mHelper.mFloatBoxType,
+                        mHelper.mFloatPrimitiveType)) {
+                    return;
+                }
+                break;
+            case LONG_PROPERTY_CLASS:
+                if (mHelper.isFieldOfExactType(property, mHelper.mIntegerBoxType,
+                        mHelper.mIntPrimitiveType, mHelper.mLongBoxType,
+                        mHelper.mLongPrimitiveType)) {
+                    return;
+                }
+                break;
+            case STRING_PROPERTY_CLASS:
+                if (mHelper.isFieldOfExactType(property, mHelper.mStringType)) {
+                    return;
+                }
+                break;
+            default:
+                // do nothing
+        }
+        throw new ProcessingException(
+                "Property Annotation " + propertyClass.getClassFullPath() + " doesn't accept the "
+                        + "data type of property field " + property.getSimpleName(), property);
+    }
+
+    /**
+     * Returns the {@link PropertyClass} with {@code annotationFq} as full class path, and {@code
+     * null} if failed to find such a {@link PropertyClass}.
+     */
+    @Nullable
+    private PropertyClass getPropertyClass(@Nullable String annotationFq) {
+        for (PropertyClass propertyClass : PropertyClass.values()) {
+            if (propertyClass.isPropertyClass(annotationFq)) {
+                return propertyClass;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Chooses how to access the given field for read and write, subject to our requirements for all
+     * AppSearch-managed class fields:
+     *
+     * <p>For read: visible field, or visible getter
+     *
+     * <p>For write: visible mutable field, or visible setter, or visible creation method
+     * accepting at minimum all fields that aren't mutable and have no visible setter.
+     *
+     * @throws ProcessingException if no access type is possible for the given field
+     */
+    private void chooseAccessKinds(@NonNull VariableElement field)
+            throws ProcessingException {
+        // Choose get access
+        String fieldName = field.getSimpleName().toString();
+        Set<Modifier> modifiers = field.getModifiers();
+        if (modifiers.contains(Modifier.PRIVATE)) {
+            findGetter(fieldName);
+            mReadKinds.put(field, ReadKind.GETTER);
+        } else {
+            mReadKinds.put(field, ReadKind.FIELD);
+        }
+
+        // Choose set access
+        if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.FINAL)
+                || modifiers.contains(Modifier.STATIC)) {
+            // Try to find a setter. If we can't find one, mark the WriteKind as {@code
+            // CREATION_METHOD}. We don't know if this is true yet, the creation methods will be
+            // inspected in a subsequent pass.
+            try {
+                findSetter(fieldName);
+                mWriteKinds.put(field, WriteKind.SETTER);
+            } catch (ProcessingException e) {
+                // We'll look for a creation method, so we may still be able to set this field,
+                // but it's more likely the developer configured the setter incorrectly. Keep
+                // the exception around to include it in the report if no creation method is found.
+                mWriteWhyCreationMethod.put(field, e);
+                mWriteKinds.put(field, WriteKind.CREATION_METHOD);
+            }
+        } else {
+            mWriteKinds.put(field, WriteKind.FIELD);
+        }
+    }
+
+    private void scanCreationMethods(Set<ExecutableElement> creationMethods)
+            throws ProcessingException {
+        // Maps field name to Element.
+        // If this is changed to a HashSet, we might report errors to the developer in a different
+        // order about why a field was written via creation method.
+        Map<String, VariableElement> creationMethodWrittenFields = new LinkedHashMap<>();
+        for (Map.Entry<VariableElement, WriteKind> it : mWriteKinds.entrySet()) {
+            if (it.getValue() == WriteKind.CREATION_METHOD) {
+                String name = it.getKey().getSimpleName().toString();
+                creationMethodWrittenFields.put(name, it.getKey());
+            }
+        }
+
+        // Maps normalized field name to real field name.
+        Map<String, String> normalizedToRawFieldName = new HashMap<>();
+        for (String fieldName : mAllAppSearchFields.keySet()) {
+            normalizedToRawFieldName.put(getNormalizedFieldName(fieldName), fieldName);
+        }
+
+        Map<ExecutableElement, String> whyNotCreationMethod = new HashMap<>();
+        creationMethodSearch:
+        for (ExecutableElement method : creationMethods) {
+            if (method.getModifiers().contains(Modifier.PRIVATE)) {
+                whyNotCreationMethod.put(method, "Creation method is private");
+                continue creationMethodSearch;
+            }
+            // The field name of each field that goes into the creation method, in the order they
+            // are declared in the creation method signature.
+            List<String> creationMethodParamFields = new ArrayList<>();
+            Set<String> remainingFields = new HashSet<>(creationMethodWrittenFields.keySet());
+            for (VariableElement parameter : method.getParameters()) {
+                String paramName = parameter.getSimpleName().toString();
+                String fieldName = normalizedToRawFieldName.get(paramName);
+                if (fieldName == null) {
+                    whyNotCreationMethod.put(
+                            method,
+                            "Parameter \"" + paramName + "\" is not an AppSearch parameter; don't "
+                                    + "know how to supply it.");
+                    continue creationMethodSearch;
+                }
+                remainingFields.remove(fieldName);
+                creationMethodParamFields.add(fieldName);
+            }
+            if (!remainingFields.isEmpty()) {
+                whyNotCreationMethod.put(
+                        method,
+                        "This method doesn't have parameters for the following fields: "
+                                + remainingFields);
+                continue creationMethodSearch;
+            }
+            // Found one!
+            mChosenCreationMethod = method;
+            mChosenCreationMethodParams = creationMethodParamFields;
+            return;
+        }
+
+        // If we got here, we couldn't find any creation methods.
+        ProcessingException e =
+                new ProcessingException(
+                        "Failed to find any suitable creation methods to build this class. See "
+                                + "warnings for details.", mClass);
+
+        // Inform the developer why we started looking for creation methods in the first place.
+        for (VariableElement field : creationMethodWrittenFields.values()) {
+            ProcessingException warning = mWriteWhyCreationMethod.get(field);
+            if (warning != null) {
+                e.addWarning(warning);
+            }
+        }
+
+        // Inform the developer about why each creation method we considered was rejected.
+        for (Map.Entry<ExecutableElement, String> it : whyNotCreationMethod.entrySet()) {
+            ProcessingException warning = new ProcessingException(
+                    "Cannot use this creation method to construct the class: " + it.getValue(),
+                    it.getKey());
+            e.addWarning(warning);
+        }
+
+        throw e;
+    }
+
+    /** Finds getter function for a private field. */
+    private void findGetter(@NonNull String fieldName) throws ProcessingException {
+        ProcessingException e = new ProcessingException(
+                "Field cannot be read: it is private and we failed to find a suitable getter "
+                        + "for field \"" + fieldName + "\"",
+                mAllAppSearchFields.get(fieldName));
+
+        for (ExecutableElement method : mAllMethods) {
+            String methodName = method.getSimpleName().toString();
+            String normalizedFieldName = getNormalizedFieldName(fieldName);
+            if (methodName.equals(normalizedFieldName)
+                    || methodName.equals("get"
+                    + normalizedFieldName.substring(0, 1).toUpperCase()
+                    + normalizedFieldName.substring(1))) {
+                if (method.getModifiers().contains(Modifier.PRIVATE)) {
+                    e.addWarning(new ProcessingException(
+                            "Getter cannot be used: private visibility", method));
+                    continue;
+                }
+                if (!method.getParameters().isEmpty()) {
+                    e.addWarning(new ProcessingException(
+                            "Getter cannot be used: should take no parameters", method));
+                    continue;
+                }
+                // Found one!
+                mGetterMethods.put(fieldName, method);
+                return;
+            }
+        }
+
+        // Broke out of the loop without finding anything.
+        throw e;
+    }
+
+    /** Finds setter function for a private field. */
+    private void findSetter(@NonNull String fieldName) throws ProcessingException {
+        // We can't report setter failure until we've searched the creation methods, so this
+        // message is anticipatory and should be buffered by the caller.
+        ProcessingException e = new ProcessingException(
+                "Field cannot be written directly or via setter because it is private, final, or "
+                        + "static, and we failed to find a suitable setter for field \""
+                        + fieldName
+                        + "\". Trying to find a suitable creation method.",
+                mAllAppSearchFields.get(fieldName));
+
+        for (ExecutableElement method : mAllMethods) {
+            String methodName = method.getSimpleName().toString();
+            String normalizedFieldName = getNormalizedFieldName(fieldName);
+            if (methodName.equals(normalizedFieldName)
+                    || methodName.equals("set"
+                    + normalizedFieldName.substring(0, 1).toUpperCase()
+                    + normalizedFieldName.substring(1))) {
+                if (method.getModifiers().contains(Modifier.PRIVATE)) {
+                    e.addWarning(new ProcessingException(
+                            "Setter cannot be used: private visibility", method));
+                    continue;
+                }
+                if (method.getParameters().size() != 1) {
+                    e.addWarning(new ProcessingException(
+                            "Setter cannot be used: takes " + method.getParameters().size()
+                                    + " parameters instead of 1",
+                            method));
+                    continue;
+                }
+                // Found one!
+                mSetterMethods.put(fieldName, method);
+                return;
+            }
+        }
+
+        // Broke out of the loop without finding anything.
+        throw e;
+    }
+
+    /**
+     * Produces the canonical name of a field (which is used as the default property name as well as
+     * to find accessors) by removing prefixes and suffixes of common conventions.
+     */
+    private String getNormalizedFieldName(String fieldName) {
+        if (fieldName.length() < 2) {
+            return fieldName;
+        }
+
+        // Handle convention of having field names start with m
+        // (e.g. String mName; public String getName())
+        if (fieldName.charAt(0) == 'm' && Character.isUpperCase(fieldName.charAt(1))) {
+            return fieldName.substring(1, 2).toLowerCase() + fieldName.substring(2);
+        }
+
+        // Handle convention of having field names start with _
+        // (e.g. String _name; public String getName())
+        if (fieldName.charAt(0) == '_'
+                && fieldName.charAt(1) != '_'
+                && Character.isLowerCase(fieldName.charAt(1))) {
+            return fieldName.substring(1);
+        }
+
+        // Handle convention of having field names end with _
+        // (e.g. String name_; public String getName())
+        if (fieldName.charAt(fieldName.length() - 1) == '_'
+                && fieldName.charAt(fieldName.length() - 2) != '_') {
+            return fieldName.substring(0, fieldName.length() - 1);
+        }
+
+        return fieldName;
+    }
+}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
index 7ccdc53..05b9752 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.compiler;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -30,9 +32,11 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.ArrayType;
@@ -42,28 +46,28 @@
 import javax.lang.model.util.Types;
 
 /**
- * Generates java code for a translator from a {@link androidx.appsearch.app.GenericDocument} to
- * a data class.
+ * Generates java code for a translator from a {@code androidx.appsearch.app.GenericDocument} to
+ * an instance of a class annotated with {@code androidx.appsearch.annotation.Document}.
  */
 class FromGenericDocumentCodeGenerator {
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
-
-    public static void generate(
-            @NonNull ProcessingEnvironment env,
-            @NonNull AppSearchDocumentModel model,
-            @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
-        new FromGenericDocumentCodeGenerator(env, model).generate(classBuilder);
-    }
+    private final DocumentModel mModel;
 
     private FromGenericDocumentCodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model) {
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
         mEnv = env;
         mHelper = new IntrospectionHelper(env);
         mModel = model;
     }
 
+    public static void generate(
+            @NonNull ProcessingEnvironment env,
+            @NonNull DocumentModel model,
+            @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
+        new FromGenericDocumentCodeGenerator(env, model).generate(classBuilder);
+    }
+
     private void generate(TypeSpec.Builder classBuilder) throws ProcessingException {
         classBuilder.addMethod(createFromGenericDocumentMethod());
     }
@@ -80,14 +84,21 @@
 
         unpackSpecialFields(methodBuilder);
 
-        // Unpack properties from the GenericDocument into the format desired by the data class
+        // Unpack properties from the GenericDocument into the format desired by the document class
         for (Map.Entry<String, VariableElement> entry : mModel.getPropertyFields().entrySet()) {
             fieldFromGenericDoc(methodBuilder, entry.getKey(), entry.getValue());
         }
 
-        // Create an instance of the data class via the chosen constructor
-        methodBuilder.addStatement(
-                "$T dataClass = new $T($L)", classType, classType, getConstructorParams());
+        // Create an instance of the document class via the chosen create method.
+        if (mModel.getChosenCreationMethod().getKind() == ElementKind.CONSTRUCTOR) {
+            methodBuilder.addStatement(
+                    "$T document = new $T($L)", classType, classType, getCreationMethodParams());
+        } else {
+            methodBuilder.addStatement(
+                    "$T document = $T.$L($L)", classType, classType,
+                    mModel.getChosenCreationMethod().getSimpleName().toString(),
+                    getCreationMethodParams());
+        }
 
         // Assign all fields which weren't set in the constructor
         for (String field : mModel.getAllFields().keySet()) {
@@ -97,13 +108,13 @@
             }
         }
 
-        methodBuilder.addStatement("return dataClass");
+        methodBuilder.addStatement("return document");
         return methodBuilder.build();
     }
 
     /**
-     * Converts a field from a {@link androidx.appsearch.app.GenericDocument} into a format suitable
-     * for the data class.
+     * Converts a field from a {@code androidx.appsearch.app.GenericDocument} into a format suitable
+     * for the document class.
      */
     private void fieldFromGenericDoc(
             @NonNull MethodSpec.Builder builder,
@@ -121,7 +132,7 @@
         //       conversion of the collection elements is needed. We can use Arrays#asList for this.
         //
         //   1c: ListForLoopCallFromGenericDocument
-        //       List contains a class which is annotated with @AppSearchDocument.
+        //       List contains a class which is annotated with @Document.
         //       We have to convert this from an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -143,7 +154,7 @@
         //       We can directly use this field with no conversion.
         //
         //   2c: ArrayForLoopCallFromGenericDocument
-        //       Array is of a class which is annotated with @AppSearchDocument.
+        //       Array is of a class which is annotated with @Document.
         //       We have to convert this from an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -167,11 +178,9 @@
         //       needed
         //
         //   3c: FieldCallFromGenericDocument
-        //       Field is of a class which is annotated with @AppSearchDocument.
+        //       Field is of a class which is annotated with @Document.
         //       We have to convert this from a GenericDocument through the standard conversion
         //       machinery.
-        //
-        //   3x: Field is of any other kind of class. This is unsupported and compilation fails.
 
         String propertyName = mModel.getPropertyName(property);
         if (tryConvertToList(builder, fieldName, propertyName, property)) {
@@ -206,9 +215,9 @@
         CodeBlock.Builder builder = CodeBlock.builder();
         if (!tryListForLoopAssign(builder, fieldName, propertyName, propertyType, listTypeName)// 1a
                 && !tryListCallArraysAsList(
-                        builder, fieldName, propertyName, propertyType, listTypeName)          // 1b
+                builder, fieldName, propertyName, propertyType, listTypeName)          // 1b
                 && !tryListForLoopCallFromGenericDocument(
-                        builder, fieldName, propertyName, propertyType, listTypeName)) {       // 1c
+                builder, fieldName, propertyName, propertyType, listTypeName)) {       // 1c
             // Scenario 1x
             throw new ProcessingException(
                     "Unhandled in property type (1x): " + property.asType().toString(), property);
@@ -322,7 +331,7 @@
     }
 
     //   1c: ListForLoopCallFromGenericDocument
-    //       List contains a class which is annotated with @AppSearchDocument.
+    //       List contains a class which is annotated with @Document.
     //       We have to convert this from an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryListForLoopCallFromGenericDocument(
@@ -330,7 +339,7 @@
             @NonNull String fieldName,
             @NonNull String propertyName,
             @NonNull TypeMirror propertyType,
-            @NonNull ParameterizedTypeName listTypeName)  {
+            @NonNull ParameterizedTypeName listTypeName) {
         Types typeUtil = mEnv.getTypeUtils();
         CodeBlock.Builder body = CodeBlock.builder();
 
@@ -340,9 +349,9 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            getDocumentAnnotation(element);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 1c
+            // The propertyType doesn't have @Document annotation, this is not a type 1c
             // list.
             return false;
         }
@@ -356,17 +365,15 @@
 
         // If not null, iterate and assign
         body.add("if ($NCopy != null) {\n", fieldName).indent();
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
-        body.addStatement("$NConv = new $T<>($NCopy.length)", fieldName, ArrayList.class,
-                fieldName);
+        body.addStatement(
+                "$NConv = new $T<>($NCopy.length)", fieldName, ArrayList.class, fieldName);
 
-        body.add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent();
-        body.addStatement("$NConv.add(factory.fromGenericDocument($NCopy[i]))", fieldName,
-                fieldName);
-        body.unindent().add("}\n");
+        body
+                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv.add($NCopy[i].toDocumentClass($T.class))",
+                        fieldName, fieldName, propertyType)
+                .unindent().add("}\n");
 
         body.unindent().add("}\n");  //  if ($NCopy != null) {
         method.add(body.build());
@@ -397,7 +404,7 @@
         if (!tryArrayForLoopAssign(builder, fieldName, propertyName, propertyType)             // 2a
                 && !tryArrayUseDirectly(builder, fieldName, propertyName, propertyType)        // 2b
                 && !tryArrayForLoopCallFromGenericDocument(
-                        builder, fieldName, propertyName, propertyType)) {                     // 2c
+                builder, fieldName, propertyName, propertyType)) {                     // 2c
             // Scenario 2x
             throw new ProcessingException(
                     "Unhandled in property type (2x): " + property.asType().toString(), property);
@@ -525,7 +532,7 @@
     }
 
     //   2c: ArrayForLoopCallFromGenericDocument
-    //       Array is of a class which is annotated with @AppSearchDocument.
+    //       Array is of a class which is annotated with @Document.
     //       We have to convert this from an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryArrayForLoopCallFromGenericDocument(
@@ -542,9 +549,9 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            getDocumentAnnotation(element);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 2c
+            // The propertyType doesn't have @Document annotation, this is not a type 2c
             // array.
             return false;
         }
@@ -562,15 +569,13 @@
         // If not null, iterate and assign
         body.add("if ($NCopy != null) {\n", fieldName).indent();
         body.addStatement("$NConv = new $T[$NCopy.length]", fieldName, propertyType, fieldName);
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
 
-        body.add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent();
-        body.addStatement("$NConv[i] = factory.fromGenericDocument($NCopy[i])", fieldName,
-                fieldName);
-        body.unindent().add("}\n");
+        body
+                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv[i] = $NCopy[i].toDocumentClass($T.class)",
+                        fieldName, fieldName, propertyType)
+                .unindent().add("}\n");
 
         body.unindent().add("}\n");  //  if ($NCopy != null) {
         method.add(body.build());
@@ -592,12 +597,10 @@
         if (!tryFieldUseDirectlyWithNullCheck(
                 builder, fieldName, propertyName, property.asType())  // 3a
                 && !tryFieldUseDirectlyWithoutNullCheck(
-                        builder, fieldName, propertyName, property.asType()) // 3b
+                builder, fieldName, propertyName, property.asType()) // 3b
                 && !tryFieldCallFromGenericDocument(
-                        builder, fieldName, propertyName, property.asType())) {   // 3c
-            // Scenario 3x
-            throw new ProcessingException(
-                    "Unhandled in property type (3x): " + property.asType().toString(), property);
+                builder, fieldName, propertyName, property.asType())) {   // 3c
+            throw new ProcessingException("Unhandled property type.", property);
         }
         method.addCode(builder.build());
     }
@@ -716,7 +719,7 @@
     }
 
     //   3c: FieldCallFromGenericDocument
-    //       Field is of a class which is annotated with @AppSearchDocument.
+    //       Field is of a class which is annotated with @Document.
     //       We have to convert this from a GenericDocument through the standard conversion
     //       machinery.
     private boolean tryFieldCallFromGenericDocument(
@@ -733,9 +736,9 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            getDocumentAnnotation(element);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 3c
+            // The propertyType doesn't have @Document annotation, this is not a type 3c
             // field.
             return false;
         }
@@ -745,23 +748,21 @@
 
         body.addStatement("$T $NConv = null", propertyType, fieldName);
         // If not null, assign
-        body.add("if ($NCopy != null) {\n", fieldName).indent();
-
-        body.addStatement("$NConv = $T.getInstance().getOrCreateFactory($T.class)"
-                        + ".fromGenericDocument($NCopy)", fieldName,
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType,
-                fieldName);
-
-        body.unindent().add("}\n");
+        body
+                .add("if ($NCopy != null) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv = $NCopy.toDocumentClass($T.class)",
+                        fieldName, fieldName, propertyType)
+                .unindent().add("}\n");
 
         method.add(body.build());
 
         return true;
     }
 
-    private CodeBlock getConstructorParams() {
+    private CodeBlock getCreationMethodParams() {
         CodeBlock.Builder builder = CodeBlock.builder();
-        List<String> params = mModel.getChosenConstructorParams();
+        List<String> params = mModel.getChosenCreationMethodParams();
         if (params.size() > 0) {
             builder.add("$NConv", params.get(0));
         }
@@ -772,15 +773,15 @@
     }
 
     private void unpackSpecialFields(@NonNull MethodSpec.Builder method) {
-        for (AppSearchDocumentModel.SpecialField specialField :
-                AppSearchDocumentModel.SpecialField.values()) {
+        for (DocumentModel.SpecialField specialField :
+                DocumentModel.SpecialField.values()) {
             String fieldName = mModel.getSpecialFieldName(specialField);
             if (fieldName == null) {
-                continue;  // The data class doesn't have this field, so no need to unpack it.
+                continue;  // The document class doesn't have this field, so no need to unpack it.
             }
             switch (specialField) {
-                case URI:
-                    method.addStatement("String $NConv = genericDoc.getUri()", fieldName);
+                case ID:
+                    method.addStatement("String $NConv = genericDoc.getId()", fieldName);
                     break;
                 case NAMESPACE:
                     method.addStatement("String $NConv = genericDoc.getNamespace()", fieldName);
@@ -801,12 +802,12 @@
 
     @Nullable
     private CodeBlock createAppSearchFieldWrite(@NonNull String fieldName) {
-        switch (mModel.getFieldWriteKind(fieldName)) {
+        switch (Objects.requireNonNull(mModel.getFieldWriteKind(fieldName))) {
             case FIELD:
-                return CodeBlock.of("dataClass.$N = $NConv", fieldName, fieldName);
+                return CodeBlock.of("document.$N = $NConv", fieldName, fieldName);
             case SETTER:
-                String setter = mModel.getAccessorName(fieldName, /*get=*/ false);
-                return CodeBlock.of("dataClass.$N($NConv)", setter, fieldName);
+                String setter = mModel.getSetterForField(fieldName).getSimpleName().toString();
+                return CodeBlock.of("document.$N($NConv)", setter, fieldName);
             default:
                 return null;  // Constructor params should already have been set
         }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index 0488796..dd9b510 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -17,6 +17,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 
 import com.squareup.javapoet.ClassName;
 
@@ -24,12 +25,16 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.AnnotationMirror;
 import javax.lang.model.element.AnnotationValue;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
 import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
 import javax.lang.model.util.Elements;
@@ -37,28 +42,23 @@
 
 /**
  * Utilities for working with data structures representing parsed Java code.
+ *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class IntrospectionHelper {
+    @VisibleForTesting
+    static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
     static final String APPSEARCH_PKG = "androidx.appsearch.app";
     static final String APPSEARCH_EXCEPTION_PKG = "androidx.appsearch.exceptions";
     static final String APPSEARCH_EXCEPTION_SIMPLE_NAME = "AppSearchException";
-    static final String APP_SEARCH_DOCUMENT_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument";
-    static final String URI_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Uri";
-    static final String NAMESPACE_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Namespace";
+    static final String DOCUMENT_ANNOTATION_CLASS = "androidx.appsearch.annotation.Document";
+    static final String ID_CLASS = "androidx.appsearch.annotation.Document.Id";
+    static final String NAMESPACE_CLASS = "androidx.appsearch.annotation.Document.Namespace";
     static final String CREATION_TIMESTAMP_MILLIS_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.CreationTimestampMillis";
-    static final String TTL_MILLIS_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.TtlMillis";
-    static final String SCORE_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Score";
-    static final String PROPERTY_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Property";
-
+            "androidx.appsearch.annotation.Document.CreationTimestampMillis";
+    static final String TTL_MILLIS_CLASS = "androidx.appsearch.annotation.Document.TtlMillis";
+    static final String SCORE_CLASS = "androidx.appsearch.annotation.Document.Score";
     final TypeMirror mCollectionType;
     final TypeMirror mListType;
     final TypeMirror mStringType;
@@ -74,42 +74,96 @@
     final TypeMirror mBooleanPrimitiveType;
     final TypeMirror mByteBoxType;
     final TypeMirror mByteBoxArrayType;
+    final TypeMirror mBytePrimitiveType;
     final TypeMirror mBytePrimitiveArrayType;
-
     private final ProcessingEnvironment mEnv;
+    private final Types mTypeUtils;
 
     IntrospectionHelper(ProcessingEnvironment env) {
         mEnv = env;
 
         Elements elementUtil = env.getElementUtils();
-        Types typeUtil = env.getTypeUtils();
+        mTypeUtils = env.getTypeUtils();
         mCollectionType = elementUtil.getTypeElement(Collection.class.getName()).asType();
         mListType = elementUtil.getTypeElement(List.class.getName()).asType();
         mStringType = elementUtil.getTypeElement(String.class.getName()).asType();
         mIntegerBoxType = elementUtil.getTypeElement(Integer.class.getName()).asType();
-        mIntPrimitiveType = typeUtil.unboxedType(mIntegerBoxType);
+        mIntPrimitiveType = mTypeUtils.unboxedType(mIntegerBoxType);
         mLongBoxType = elementUtil.getTypeElement(Long.class.getName()).asType();
-        mLongPrimitiveType = typeUtil.unboxedType(mLongBoxType);
+        mLongPrimitiveType = mTypeUtils.unboxedType(mLongBoxType);
         mFloatBoxType = elementUtil.getTypeElement(Float.class.getName()).asType();
-        mFloatPrimitiveType = typeUtil.unboxedType(mFloatBoxType);
+        mFloatPrimitiveType = mTypeUtils.unboxedType(mFloatBoxType);
         mDoubleBoxType = elementUtil.getTypeElement(Double.class.getName()).asType();
-        mDoublePrimitiveType = typeUtil.unboxedType(mDoubleBoxType);
+        mDoublePrimitiveType = mTypeUtils.unboxedType(mDoubleBoxType);
         mBooleanBoxType = elementUtil.getTypeElement(Boolean.class.getName()).asType();
-        mBooleanPrimitiveType = typeUtil.unboxedType(mBooleanBoxType);
+        mBooleanPrimitiveType = mTypeUtils.unboxedType(mBooleanBoxType);
         mByteBoxType = elementUtil.getTypeElement(Byte.class.getName()).asType();
-        mBytePrimitiveArrayType = typeUtil.getArrayType(typeUtil.getPrimitiveType(TypeKind.BYTE));
-        mByteBoxArrayType = typeUtil.getArrayType(mByteBoxType);
+        mByteBoxArrayType = mTypeUtils.getArrayType(mByteBoxType);
+        mBytePrimitiveType = mTypeUtils.unboxedType(mByteBoxType);
+        mBytePrimitiveArrayType = mTypeUtils.getArrayType(mBytePrimitiveType);
     }
 
-    public AnnotationMirror getAnnotation(@NonNull Element element, @NonNull String fqClass)
+    /**
+     * Returns {@code androidx.appsearch.annotation.Document} annotation element from the input
+     * element's annotations.
+     *
+     * @throws ProcessingException if no such annotation is found.
+     */
+    @NonNull
+    public static AnnotationMirror getDocumentAnnotation(@NonNull Element element)
             throws ProcessingException {
+        Objects.requireNonNull(element);
         for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
             String annotationFq = annotation.getAnnotationType().toString();
-            if (fqClass.equals(annotationFq)) {
+            if (IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.equals(annotationFq)) {
                 return annotation;
             }
         }
-        throw new ProcessingException("Missing annotation " + fqClass, element);
+        throw new ProcessingException(
+                "Missing annotation " + IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS, element);
+    }
+
+    /** Checks whether the property data type is one of the valid types. */
+    public boolean isFieldOfExactType(VariableElement property, TypeMirror... validTypes) {
+        TypeMirror propertyType = property.asType();
+        for (TypeMirror validType : validTypes) {
+            if (propertyType.getKind() == TypeKind.ARRAY) {
+                if (mTypeUtils.isSameType(
+                        ((ArrayType) propertyType).getComponentType(), validType)) {
+                    return true;
+                }
+            } else if (mTypeUtils.isAssignable(mTypeUtils.erasure(propertyType), mCollectionType)) {
+                if (mTypeUtils.isSameType(
+                        ((DeclaredType) propertyType).getTypeArguments().get(0), validType)) {
+                    return true;
+                }
+            } else if (mTypeUtils.isSameType(property.asType(), validType)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether the property data class has {@code androidx.appsearch.annotation.Document
+     * .DocumentProperty} annotation.
+     */
+    public boolean isFieldOfDocumentType(VariableElement property) {
+        TypeMirror propertyType = property.asType();
+        try {
+            if (propertyType.getKind() == TypeKind.ARRAY) {
+                getDocumentAnnotation(
+                        mTypeUtils.asElement(((ArrayType) property.asType()).getComponentType()));
+            } else if (mTypeUtils.isAssignable(mTypeUtils.erasure(propertyType), mCollectionType)) {
+                getDocumentAnnotation(mTypeUtils.asElement(
+                        ((DeclaredType) propertyType).getTypeArguments().get(0)));
+            } else {
+                getDocumentAnnotation(mTypeUtils.asElement(propertyType));
+            }
+        } catch (ProcessingException e) {
+            return false;
+        }
+        return true;
     }
 
     public Map<String, Object> getAnnotationParams(@NonNull AnnotationMirror annotation) {
@@ -124,6 +178,24 @@
         return ret;
     }
 
+    /**
+     * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
+     * for inner class Foo.Bar.
+     */
+    public ClassName getDocumentClassFactoryForClass(String pkg, String className) {
+        String genClassName = GEN_CLASS_PREFIX + className.replace(".", "$$__");
+        return ClassName.get(pkg, genClassName);
+    }
+
+    /**
+     * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
+     * for inner class Foo.Bar.
+     */
+    public ClassName getDocumentClassFactoryForClass(ClassName clazz) {
+        String className = clazz.canonicalName().substring(clazz.packageName().length() + 1);
+        return getDocumentClassFactoryForClass(clazz.packageName(), className);
+    }
+
     public ClassName getAppSearchClass(String clazz, String... nested) {
         return ClassName.get(APPSEARCH_PKG, clazz, nested);
     }
@@ -131,4 +203,27 @@
     public ClassName getAppSearchExceptionClass() {
         return ClassName.get(APPSEARCH_EXCEPTION_PKG, APPSEARCH_EXCEPTION_SIMPLE_NAME);
     }
+
+    enum PropertyClass {
+        BOOLEAN_PROPERTY_CLASS("androidx.appsearch.annotation.Document.BooleanProperty"),
+        BYTES_PROPERTY_CLASS("androidx.appsearch.annotation.Document.BytesProperty"),
+        DOCUMENT_PROPERTY_CLASS("androidx.appsearch.annotation.Document.DocumentProperty"),
+        DOUBLE_PROPERTY_CLASS("androidx.appsearch.annotation.Document.DoubleProperty"),
+        LONG_PROPERTY_CLASS("androidx.appsearch.annotation.Document.LongProperty"),
+        STRING_PROPERTY_CLASS("androidx.appsearch.annotation.Document.StringProperty");
+
+        private final String mClassFullPath;
+
+        PropertyClass(String classFullPath) {
+            mClassFullPath = classFullPath;
+        }
+
+        String getClassFullPath() {
+            return mClassFullPath;
+        }
+
+        boolean isPropertyClass(String annotationFq) {
+            return mClassFullPath.equals(annotationFq);
+        }
+    }
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/MissingTypeException.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/MissingTypeException.java
new file mode 100644
index 0000000..918038f
--- /dev/null
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/MissingTypeException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.compiler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import javax.lang.model.element.Element;
+
+/**
+ * An exception thrown from the appsearch annotation processor to indicate a type element is not
+ * found due to it being possibly generated at a later annotation processing round.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+final class MissingTypeException extends Exception {
+    @NonNull
+    private final Element mTypeElement;
+
+    MissingTypeException(@NonNull Element typeElement) {
+        super("Type " + typeElement.getSimpleName() + " is not present");
+        mTypeElement = typeElement;
+    }
+
+    @NonNull
+    Element getTypeName() {
+        return mTypeElement;
+    }
+}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index eca9c5a..3693646 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -42,17 +42,17 @@
 class SchemaCodeGenerator {
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
+    private final DocumentModel mModel;
 
     public static void generate(
             @NonNull ProcessingEnvironment env,
-            @NonNull AppSearchDocumentModel model,
+            @NonNull DocumentModel model,
             @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
         new SchemaCodeGenerator(env, model).generate(classBuilder);
     }
 
     private SchemaCodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model) {
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
         mEnv = env;
         mHelper = new IntrospectionHelper(env);
         mModel = model;
@@ -60,17 +60,17 @@
 
     private void generate(@NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
         classBuilder.addField(
-                FieldSpec.builder(String.class, "SCHEMA_TYPE")
-                        .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
+                FieldSpec.builder(String.class, "SCHEMA_NAME")
+                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                         .initializer("$S", mModel.getSchemaName())
                         .build());
 
         classBuilder.addMethod(
-                MethodSpec.methodBuilder("getSchemaType")
+                MethodSpec.methodBuilder("getSchemaName")
                         .addModifiers(Modifier.PUBLIC)
                         .returns(TypeName.get(mHelper.mStringType))
                         .addAnnotation(Override.class)
-                        .addStatement("return SCHEMA_TYPE")
+                        .addStatement("return SCHEMA_NAME")
                         .build());
 
         classBuilder.addMethod(
@@ -85,7 +85,7 @@
 
     private CodeBlock createSchemaInitializer() throws ProcessingException {
         CodeBlock.Builder codeBlock = CodeBlock.builder()
-                .add("new $T(SCHEMA_TYPE)", mHelper.getAppSearchClass("AppSearchSchema", "Builder"))
+                .add("new $T(SCHEMA_NAME)", mHelper.getAppSearchClass("AppSearchSchema", "Builder"))
                 .indent();
         for (VariableElement property : mModel.getPropertyFields().values()) {
             codeBlock.add("\n.addProperty($L)", createPropertySchema(property));
@@ -96,22 +96,14 @@
 
     private CodeBlock createPropertySchema(@NonNull VariableElement property)
             throws ProcessingException {
-        AnnotationMirror annotation =
-                mHelper.getAnnotation(property, IntrospectionHelper.PROPERTY_CLASS);
+        AnnotationMirror annotation = mModel.getPropertyAnnotation(property);
         Map<String, Object> params = mHelper.getAnnotationParams(annotation);
 
-        // Start the builder for that property
-        String propertyName = mModel.getPropertyName(property);
-        CodeBlock.Builder codeBlock = CodeBlock.builder()
-                .add("new $T($S)",
-                        mHelper.getAppSearchClass("AppSearchSchema", "PropertyConfig", "Builder"),
-                        propertyName)
-                .indent();
-
         // Find the property type
         Types typeUtil = mEnv.getTypeUtils();
         TypeMirror propertyType;
         boolean repeated = false;
+        boolean isPropertyString = false;
         boolean isPropertyDocument = false;
         if (property.asType().getKind() == TypeKind.ERROR) {
             throw new ProcessingException("Property type unknown to java compiler", property);
@@ -136,42 +128,47 @@
         } else {
             propertyType = property.asType();
         }
-        ClassName propertyTypeEnum;
+        ClassName propertyClass;
         if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_STRING");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "StringPropertyConfig");
+            isPropertyString = true;
         } else if (typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)
                 || typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_INT64");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "LongPropertyConfig");
         } else if (typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)
                 || typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_DOUBLE");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "DoublePropertyConfig");
         } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_BOOLEAN");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "BooleanPropertyConfig");
         } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)
                 || typeUtil.isSameType(propertyType, mHelper.mByteBoxArrayType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_BYTES");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "BytesPropertyConfig");
         } else {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_DOCUMENT");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "DocumentPropertyConfig");
             isPropertyDocument = true;
         }
-        codeBlock.add("\n.setDataType($T)", propertyTypeEnum);
 
+        // Start the builder for the property
+        String propertyName = mModel.getPropertyName(property);
+        CodeBlock.Builder codeBlock = CodeBlock.builder();
         if (isPropertyDocument) {
-            codeBlock.add("\n.setSchemaType($T.getInstance()"
-                    + ".getOrCreateFactory($T.class).getSchemaType())",
-                    mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
+            ClassName documentClass = (ClassName) ClassName.get(propertyType);
+            ClassName documentFactoryClass = mHelper.getDocumentClassFactoryForClass(documentClass);
+            codeBlock.add(
+                    "new $T($S, $T.SCHEMA_NAME)",
+                    propertyClass.nestedClass("Builder"),
+                    propertyName,
+                    documentFactoryClass);
+        } else {
+            codeBlock.add("new $T($S)", propertyClass.nestedClass("Builder"), propertyName);
         }
+        codeBlock.indent();
+
         // Find property cardinality
         ClassName cardinalityEnum;
         if (repeated) {
@@ -186,42 +183,45 @@
         }
         codeBlock.add("\n.setCardinality($T)", cardinalityEnum);
 
-        // Find tokenizer type
-        int tokenizerType = Integer.parseInt(params.get("tokenizerType").toString());
-        if (Integer.parseInt(params.get("indexingType").toString()) == 0) {
-            //TODO(b/171857731) remove this hack after apply to Icing lib's change.
-            tokenizerType = 0;
-        }
-        ClassName tokenizerEnum;
-        if (tokenizerType == 0 || isPropertyDocument) {  // TOKENIZER_TYPE_NONE
-            //It is only valid for tokenizer_type to be 'NONE' if the data type is
-            // {@link PropertyConfig#DATA_TYPE_DOCUMENT}.
-            tokenizerEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "TOKENIZER_TYPE_NONE");
-        } else if (tokenizerType == 1) {  // TOKENIZER_TYPE_PLAIN
-            tokenizerEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "TOKENIZER_TYPE_PLAIN");
-        } else {
-            throw new ProcessingException("Unknown tokenizer type " + tokenizerType, property);
-        }
-        codeBlock.add("\n.setTokenizerType($T)", tokenizerEnum);
+        if (isPropertyString) {
+            // Find tokenizer type
+            int tokenizerType = Integer.parseInt(params.get("tokenizerType").toString());
+            if (Integer.parseInt(params.get("indexingType").toString()) == 0) {
+                //TODO(b/171857731) remove this hack after apply to Icing lib's change.
+                tokenizerType = 0;
+            }
+            ClassName tokenizerEnum;
+            if (tokenizerType == 0) {  // TOKENIZER_TYPE_NONE
+                tokenizerEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_NONE");
+            } else if (tokenizerType == 1) {  // TOKENIZER_TYPE_PLAIN
+                tokenizerEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_PLAIN");
+            } else {
+                throw new ProcessingException("Unknown tokenizer type " + tokenizerType, property);
+            }
+            codeBlock.add("\n.setTokenizerType($T)", tokenizerEnum);
 
-        // Find indexing type
-        int indexingType = Integer.parseInt(params.get("indexingType").toString());
-        ClassName indexingEnum;
-        if (indexingType == 0) {  // INDEXING_TYPE_NONE
-            indexingEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "INDEXING_TYPE_NONE");
-        } else if (indexingType == 1) {  // INDEXING_TYPE_EXACT_TERMS
-            indexingEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "INDEXING_TYPE_EXACT_TERMS");
-        } else if (indexingType == 2) {  // INDEXING_TYPE_PREFIXES
-            indexingEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "INDEXING_TYPE_PREFIXES");
-        } else {
-            throw new ProcessingException("Unknown indexing type " + indexingType, property);
+            // Find indexing type
+            int indexingType = Integer.parseInt(params.get("indexingType").toString());
+            ClassName indexingEnum;
+            if (indexingType == 0) {  // INDEXING_TYPE_NONE
+                indexingEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_NONE");
+            } else if (indexingType == 1) {  // INDEXING_TYPE_EXACT_TERMS
+                indexingEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_EXACT_TERMS");
+            } else if (indexingType == 2) {  // INDEXING_TYPE_PREFIXES
+                indexingEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_PREFIXES");
+            } else {
+                throw new ProcessingException("Unknown indexing type " + indexingType, property);
+            }
+            codeBlock.add("\n.setIndexingType($T)", indexingEnum);
+
+        } else if (isPropertyDocument) {
+            // TODO(b/177572431): Apply setIndexNestedProperties here
         }
-        codeBlock.add("\n.setIndexingType($T)", indexingEnum);
 
         // Done!
         codeBlock.add("\n.build()");
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
index b30d907..1679c84 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.compiler;
 
+import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
+
 import androidx.annotation.NonNull;
 
 import com.squareup.javapoet.CodeBlock;
@@ -27,6 +29,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.Element;
@@ -39,28 +42,29 @@
 import javax.lang.model.util.Types;
 
 /**
- * Generates java code for a translator from a data class to a
+ * Generates java code for a translator from an instance of a class annotated with
+ * {@link androidx.appsearch.annotation.Document} into a
  * {@link androidx.appsearch.app.GenericDocument}.
  */
 class ToGenericDocumentCodeGenerator {
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
-
-    public static void generate(
-            @NonNull ProcessingEnvironment env,
-            @NonNull AppSearchDocumentModel model,
-            @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
-        new ToGenericDocumentCodeGenerator(env, model).generate(classBuilder);
-    }
+    private final DocumentModel mModel;
 
     private ToGenericDocumentCodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model) {
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
         mEnv = env;
         mHelper = new IntrospectionHelper(env);
         mModel = model;
     }
 
+    public static void generate(
+            @NonNull ProcessingEnvironment env,
+            @NonNull DocumentModel model,
+            @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
+        new ToGenericDocumentCodeGenerator(env, model).generate(classBuilder);
+    }
+
     private void generate(TypeSpec.Builder classBuilder) throws ProcessingException {
         classBuilder.addMethod(createToGenericDocumentMethod());
     }
@@ -72,17 +76,19 @@
                 .addModifiers(Modifier.PUBLIC)
                 .returns(mHelper.getAppSearchClass("GenericDocument"))
                 .addAnnotation(Override.class)
-                .addParameter(classType, "dataClass")
+                .addParameter(classType, "document")
                 .addException(mHelper.getAppSearchExceptionClass());
 
-        // Construct a new GenericDocument.Builder with the schema type and URI
-        methodBuilder.addStatement("$T builder =\nnew $T<>($L, SCHEMA_TYPE)",
+        // Construct a new GenericDocument.Builder with the namespace, id, and schema type
+        methodBuilder.addStatement("$T builder =\nnew $T<>($L, $L, SCHEMA_NAME)",
                 ParameterizedTypeName.get(
                         mHelper.getAppSearchClass("GenericDocument", "Builder"),
                         WildcardTypeName.subtypeOf(Object.class)),
                 mHelper.getAppSearchClass("GenericDocument", "Builder"),
                 createAppSearchFieldRead(
-                        mModel.getSpecialFieldName(AppSearchDocumentModel.SpecialField.URI)));
+                        mModel.getSpecialFieldName(DocumentModel.SpecialField.NAMESPACE)),
+                createAppSearchFieldRead(
+                        mModel.getSpecialFieldName(DocumentModel.SpecialField.ID)));
 
         setSpecialFields(methodBuilder);
 
@@ -96,7 +102,7 @@
     }
 
     /**
-     * Converts a field from a data class into a format suitable for one of the
+     * Converts a field from a document class into a format suitable for one of the
      * {@link androidx.appsearch.app.GenericDocument.Builder#setProperty} methods.
      */
     private void fieldToGenericDoc(
@@ -117,7 +123,7 @@
         //       this.
         //
         //   1c: CollectionForLoopCallToGenericDocument
-        //       Collection contains a class which is annotated with @AppSearchDocument.
+        //       Collection contains a class which is annotated with @Document.
         //       We have to convert this into an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -141,7 +147,7 @@
         //       We can directly use this field with no conversion.
         //
         //   2c: ArrayForLoopCallToGenericDocument
-        //       Array is of a class which is annotated with @AppSearchDocument.
+        //       Array is of a class which is annotated with @Document.
         //       We have to convert this into an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -164,11 +170,9 @@
         //       We can use this field directly without testing for null.
         //
         //   3c: FieldCallToGenericDocument
-        //       Field is of a class which is annotated with @AppSearchDocument.
+        //       Field is of a class which is annotated with @Document.
         //       We have to convert this into a GenericDocument through the standard conversion
         //       machinery.
-        //
-        //   3x: Field is of any other kind of class. This is unsupported and compilation fails.
         String propertyName = mModel.getPropertyName(property);
         if (tryConvertFromCollection(method, fieldName, propertyName, property)) {
             return;
@@ -209,7 +213,7 @@
         if (!tryCollectionForLoopAssign(body, fieldName, propertyName, propertyType)           // 1a
                 && !tryCollectionCallToArray(body, fieldName, propertyName, propertyType)      // 1b
                 && !tryCollectionForLoopCallToGenericDocument(
-                        body, fieldName, propertyName, propertyType)) {                        // 1c
+                body, fieldName, propertyName, propertyType)) {                        // 1c
             // Scenario 1x
             throw new ProcessingException(
                     "Unhandled out property type (1x): " + property.asType().toString(), property);
@@ -304,7 +308,7 @@
     }
 
     //   1c: CollectionForLoopCallToGenericDocument
-    //       Collection contains a class which is annotated with @AppSearchDocument.
+    //       Collection contains a class which is annotated with @Document.
     //       We have to convert this into an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryCollectionForLoopCallToGenericDocument(
@@ -322,28 +326,28 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            getDocumentAnnotation(element);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 1c
+            // The propertyType doesn't have @Document annotation, this is not a type 1c
             // list.
             return false;
         }
 
-        body.addStatement("GenericDocument[] $NConv = new GenericDocument[$NCopy.size()]",
+        body.addStatement(
+                "GenericDocument[] $NConv = new GenericDocument[$NCopy.size()]",
                 fieldName, fieldName);
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
-
         body.addStatement("int i = 0");
-        body.add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent();
-        body.addStatement("$NConv[i++] = factory.toGenericDocument(item)", fieldName);
+        body
+                .add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent()
+                .addStatement(
+                        "$NConv[i++] = $T.fromDocumentClass(item)",
+                        fieldName, mHelper.getAppSearchClass("GenericDocument"))
+                .unindent().add("}\n");
 
-        body.unindent().add("}\n");
-
-        body.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
-                .unindent().add("}\n");   //  if ($NCopy != null) {
+        body
+                .addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
+                .unindent()
+                .add("}\n");   //  if ($NCopy != null) {
 
         method.add(body.build());
         return true;
@@ -379,7 +383,7 @@
         if (!tryArrayForLoopAssign(body, fieldName, propertyName, propertyType)                // 2a
                 && !tryArrayUseDirectly(body, fieldName, propertyName, propertyType)           // 2b
                 && !tryArrayForLoopCallToGenericDocument(
-                        body, fieldName, propertyName, propertyType)) {                        // 2c
+                body, fieldName, propertyName, propertyType)) {                        // 2c
             // Scenario 2x
             throw new ProcessingException(
                     "Unhandled out property type (2x): " + property.asType().toString(), property);
@@ -483,7 +487,7 @@
     }
 
     //   2c: ArrayForLoopCallToGenericDocument
-    //       Array is of a class which is annotated with @AppSearchDocument.
+    //       Array is of a class which is annotated with @Document.
     //       We have to convert this into an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryArrayForLoopCallToGenericDocument(
@@ -501,23 +505,22 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            getDocumentAnnotation(element);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 1c
+            // The propertyType doesn't have @Document annotation, this is not a type 1c
             // list.
             return false;
         }
 
-        body.addStatement("GenericDocument[] $NConv = new GenericDocument[$NCopy.length]",
+        body.addStatement(
+                "GenericDocument[] $NConv = new GenericDocument[$NCopy.length]",
                 fieldName, fieldName);
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
-        body.add("for (int i = 0; i < $NConv.length; i++) {\n", fieldName).indent();
-        body.addStatement("$NConv[i] = factory.toGenericDocument($NCopy[i])",
-                fieldName, fieldName);
-        body.unindent().add("}\n");
+        body
+                .add("for (int i = 0; i < $NConv.length; i++) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv[i] = $T.fromDocumentClass($NCopy[i])",
+                        fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
+                .unindent().add("}\n");
 
         body.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
                 .unindent().add("}\n");    //  if ($NCopy != null) {
@@ -540,12 +543,10 @@
         if (!tryFieldUseDirectlyWithNullCheck(
                 body, fieldName, propertyName, property.asType())  // 3a
                 && !tryFieldUseDirectlyWithoutNullCheck(
-                        body, fieldName, propertyName, property.asType())  // 3b
+                body, fieldName, propertyName, property.asType())  // 3b
                 && !tryFieldCallToGenericDocument(
-                        body, fieldName, propertyName, property.asType())) {  // 3c
-            // Scenario 3x
-            throw new ProcessingException(
-                    "Unhandled out property type (3x): " + property.asType().toString(), property);
+                body, fieldName, propertyName, property.asType())) {  // 3c
+            throw new ProcessingException("Unhandled property type.", property);
         }
         method.addCode(body.build());
     }
@@ -627,7 +628,7 @@
     }
 
     //   3c: FieldCallToGenericDocument
-    //       Field is of a class which is annotated with @AppSearchDocument.
+    //       Field is of a class which is annotated with @Document.
     //       We have to convert this into a GenericDocument through the standard conversion
     //       machinery.
     private boolean tryFieldCallToGenericDocument(
@@ -643,47 +644,39 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            getDocumentAnnotation(element);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 3c
+            // The propertyType doesn't have @Document annotation, this is not a type 3c
             // field.
             return false;
         }
-        method.addStatement("$T $NCopy = $L", propertyType, propertyName,
-                createAppSearchFieldRead(fieldName));
+        method.addStatement(
+                "$T $NCopy = $L", propertyType, fieldName, createAppSearchFieldRead(fieldName));
 
-        method.add("if ($NCopy != null) {\n", propertyName).indent();
+        method.add("if ($NCopy != null) {\n", fieldName).indent();
 
-        method.addStatement("GenericDocument $NConv = $T.getInstance().getOrCreateFactory($T.class)"
-                        + ".toGenericDocument($NCopy)", fieldName,
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType,
-                propertyName);
-        method.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName);
+        method
+                .addStatement(
+                        "GenericDocument $NConv = $T.fromDocumentClass($NCopy)",
+                        fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
+                .addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName);
 
         method.unindent().add("}\n");
         return true;
     }
 
     private void setSpecialFields(MethodSpec.Builder method) {
-        for (AppSearchDocumentModel.SpecialField specialField :
-                AppSearchDocumentModel.SpecialField.values()) {
+        for (DocumentModel.SpecialField specialField :
+                DocumentModel.SpecialField.values()) {
             String fieldName = mModel.getSpecialFieldName(specialField);
             if (fieldName == null) {
-                continue;  // The data class doesn't have this field, so no need to set it.
+                continue;  // The document class doesn't have this field, so no need to set it.
             }
             switch (specialField) {
-                case URI:
+                case ID:
                     break;  // Always provided to builder constructor; cannot be set separately.
                 case NAMESPACE:
-                    method.addCode(CodeBlock.builder()
-                            .addStatement(
-                                    "String $NCopy = $L",
-                                    fieldName, createAppSearchFieldRead(fieldName))
-                            .add("if ($NCopy != null) {\n", fieldName).indent()
-                            .addStatement("builder.setNamespace($NCopy)", fieldName)
-                            .unindent().add("}\n")
-                            .build());
-                    break;
+                    break;  // Always provided to builder constructor; cannot be set separately.
                 case CREATION_TIMESTAMP_MILLIS:
                     method.addStatement(
                             "builder.setCreationTimestampMillis($L)",
@@ -702,12 +695,12 @@
     }
 
     private CodeBlock createAppSearchFieldRead(@NonNull String fieldName) {
-        switch (mModel.getFieldReadKind(fieldName)) {
+        switch (Objects.requireNonNull(mModel.getFieldReadKind(fieldName))) {
             case FIELD:
-                return CodeBlock.of("dataClass.$N", fieldName);
+                return CodeBlock.of("document.$N", fieldName);
             case GETTER:
-                String getter = mModel.getAccessorName(fieldName, /*get=*/ true);
-                return CodeBlock.of("dataClass.$N()", getter);
+                String getter = mModel.getGetterForField(fieldName).getSimpleName().toString();
+                return CodeBlock.of("document.$N()", getter);
         }
         return null;
     }
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 1693cd6..b446553 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -16,11 +16,13 @@
 
 package androidx.appsearch.compiler;
 
+import static com.google.testing.compile.CompilationSubject.assertThat;
+
+import com.google.auto.value.processor.AutoValueProcessor;
 import com.google.common.io.CharStreams;
 import com.google.common.io.Files;
 import com.google.common.truth.Truth;
 import com.google.testing.compile.Compilation;
-import com.google.testing.compile.CompilationSubject;
 import com.google.testing.compile.Compiler;
 import com.google.testing.compile.JavaFileObjects;
 
@@ -57,9 +59,10 @@
     @Test
     public void testNonClass() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public interface Gift {}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "annotation on something other than a class");
     }
 
@@ -68,311 +71,511 @@
         Compilation compilation = compile(
                 "Wrapper",
                 "public class Wrapper {\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "private class Gift {}\n"
                         + "}  // Wrapper\n"
         );
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "annotated class is private");
+
+        assertThat(compilation).hadErrorContaining("annotated class is private");
     }
 
     @Test
-    public void testNoUri() {
+    public void testNoId() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
-                        + "public class Gift {}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "must have exactly one field annotated with @Uri");
-    }
-
-    @Test
-    public void testManyUri() {
-        Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri1;\n"
-                        + "  @AppSearchDocument.Uri String uri2;\n"
+                        + "  @Document.Namespace String namespace;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "contains multiple fields annotated @Uri");
+
+        assertThat(compilation).hadErrorContaining(
+                "must have exactly one field annotated with @Id");
+    }
+
+    @Test
+    public void testManyIds() {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id1;\n"
+                        + "  @Document.Id String id2;\n"
+                        + "}\n");
+
+        assertThat(compilation).hadErrorContaining(
+                "contains multiple fields annotated @Id");
     }
 
     @Test
     public void testManyCreationTimestamp() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis long ts1;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis long ts2;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.CreationTimestampMillis long ts1;\n"
+                        + "  @Document.CreationTimestampMillis long ts2;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @CreationTimestampMillis");
     }
 
     @Test
+    public void testNoNamespace() {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Id String id;\n"
+                        + "}\n");
+
+        assertThat(compilation).hadErrorContaining(
+                "must have exactly one field annotated with @Namespace");
+    }
+
+    @Test
     public void testManyNamespace() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Namespace String ns1;\n"
-                        + "  @AppSearchDocument.Namespace String ns2;\n"
+                        + "  @Document.Namespace String ns1;\n"
+                        + "  @Document.Namespace String ns2;\n"
+                        + "  @Document.Id String id;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @Namespace");
     }
 
     @Test
     public void testManyTtlMillis() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.TtlMillis long ts1;\n"
-                        + "  @AppSearchDocument.TtlMillis long ts2;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.TtlMillis long ts1;\n"
+                        + "  @Document.TtlMillis long ts2;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @TtlMillis");
     }
 
     @Test
     public void testManyScore() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Score int score1;\n"
-                        + "  @AppSearchDocument.Score int score2;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Score int score1;\n"
+                        + "  @Document.Score int score2;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @Score");
     }
 
     @Test
-    public void testPropertyOnField() {
+    public void testPropertyOnFieldForNonAutoValueClass() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int getPrice() { return 0; }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int getPrice() { return 0; }\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "annotation type not applicable to this kind of declaration");
+
+        assertThat(compilation).hadErrorContaining(
+                "AppSearch annotation is not applicable to methods for Non-AutoValue class");
     }
 
     @Test
     public void testCantRead_noGetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "Field cannot be read: it is private and we failed to find a suitable getter "
-                        + "named \"getPrice\"");
+                        + "for field \"price\"");
     }
 
     @Test
     public void testCantRead_privateGetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "  private int getPrice() { return 0; }\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "Field cannot be read: it is private and we failed to find a suitable getter "
-                        + "named \"getPrice\"");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
-                "Getter cannot be used: private visibility");
+                        + "for field \"price\"");
+        assertThat(compilation).hadWarningContaining("Getter cannot be used: private visibility");
     }
 
     @Test
     public void testCantRead_wrongParamGetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "  int getPrice(int n) { return 0; }\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+
+        assertThat(compilation).hadErrorContaining(
                 "Field cannot be read: it is private and we failed to find a suitable getter "
-                        + "named \"getPrice\"");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
+                        + "for field \"price\"");
+        assertThat(compilation).hadWarningContaining(
                 "Getter cannot be used: should take no parameters");
     }
 
     @Test
     public void testRead_MultipleGetters() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "  int getPrice(int n) { return 0; }\n"
                         + "  int getPrice() { return 0; }\n"
                         + "  void setPrice(int n) {}\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
+        checkEqualsGolden("Gift.java");
+    }
+
+    @Test
+    public void testGetterAndSetterFunctions_withFieldName() throws Exception {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
+                        + "  int price() { return 0; }\n"
+                        + "  void price(int n) {}\n"
+                        + "}\n");
+
+        assertThat(compilation).succeededWithoutWarnings();
+        // Check setter function is identified correctly.
+        checkResultContains(/* className= */ "Gift.java",
+                /* content= */ "builder.setPropertyLong(\"price\", document.price());");
+        // Check getter function is identified correctly.
+        checkResultContains(/* className= */ "Gift.java",
+                /* content= */ "document.price(priceConv);");
         checkEqualsGolden("Gift.java");
     }
 
     @Test
     public void testCantWrite_noSetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Failed to find any suitable constructors to build this class");
-        CompilationSubject.assertThat(compilation).hadWarningContainingMatch(
-                "Field cannot be written .* failed to find a suitable setter named \"setPrice\"");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
-                "Cannot use this constructor to construct the class: This constructor doesn't have "
+
+        assertThat(compilation).hadErrorContaining(
+                "Failed to find any suitable creation methods to build this class");
+        assertThat(compilation).hadWarningContainingMatch(
+                "Field cannot be written .* failed to find a suitable setter for field \"price\"");
+        assertThat(compilation).hadWarningContaining(
+                "Cannot use this creation method to construct the class: This method doesn't have "
                         + "parameters for the following fields: [price]");
     }
 
     @Test
     public void testCantWrite_privateSetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "  private void setPrice(int n) {}\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Failed to find any suitable constructors to build this class");
-        CompilationSubject.assertThat(compilation).hadWarningContainingMatch(
-                "Field cannot be written .* failed to find a suitable setter named \"setPrice\"");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
+
+        assertThat(compilation).hadErrorContaining(
+                "Failed to find any suitable creation methods to build this class");
+        assertThat(compilation).hadWarningContainingMatch(
+                "Field cannot be written .* failed to find a suitable setter for field \"price\"");
+        assertThat(compilation).hadWarningContaining(
                 "Setter cannot be used: private visibility");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
-                "Cannot use this constructor to construct the class: This constructor doesn't have "
+        assertThat(compilation).hadWarningContaining(
+                "Cannot use this creation method to construct the class: This method doesn't have "
                         + "parameters for the following fields: [price]");
     }
 
     @Test
     public void testCantWrite_wrongParamSetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "  void setPrice() {}\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Failed to find any suitable constructors to build this class");
-        CompilationSubject.assertThat(compilation).hadWarningContainingMatch(
-                "Field cannot be written .* failed to find a suitable setter named \"setPrice\"");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
+
+        assertThat(compilation).hadErrorContaining(
+                "Failed to find any suitable creation methods to build this class");
+        assertThat(compilation).hadWarningContainingMatch(
+                "Field cannot be written .* failed to find a suitable setter for field \"price\"");
+        assertThat(compilation).hadWarningContaining(
                 "Setter cannot be used: takes 0 parameters instead of 1");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
-                "Cannot use this constructor to construct the class: This constructor doesn't have "
+        assertThat(compilation).hadWarningContaining(
+                "Cannot use this creation method to construct the class: This method doesn't have "
                         + "parameters for the following fields: [price]");
     }
 
     @Test
     public void testWrite_multipleSetters() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "  void setPrice() {}\n"
                         + "  void setPrice(int n) {}\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
     @Test
     public void testWrite_privateConstructor() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
                         + "  private Gift() {}\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty int price;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Failed to find any suitable constructors to build this class");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
-                "Constructor is private");
+
+        assertThat(compilation).hadErrorContaining(
+                "Failed to find any suitable creation methods to build this class");
+        assertThat(compilation).hadWarningContaining("Creation method is private");
     }
 
     @Test
     public void testWrite_constructorMissingParams() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
                         + "  Gift(int price) {}\n"
-                        + "  @AppSearchDocument.Uri final String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.LongProperty int price;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Failed to find any suitable constructors to build this class");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
-                "doesn't have parameters for the following fields: [uri]");
+
+        assertThat(compilation).hadErrorContaining(
+                "Failed to find any suitable creation methods to build this class");
+        assertThat(compilation).hadWarningContaining(
+                "doesn't have parameters for the following fields: [id]");
+    }
+
+    @Test
+    public void testWrite_factoryMethodOnly() throws IOException {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  private Gift(int price, String id, String namespace) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
+                        + "    this.price = price;\n"
+                        + "  }\n"
+                        + "  public static Gift create(String id, String namespace, int price) {\n"
+                        + "    return new Gift(price, id, namespace);"
+                        + "  }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.LongProperty int price;\n"
+                        + "}\n");
+
+        assertThat(compilation).succeededWithoutWarnings();
+        checkResultContains(/* className= */ "Gift.java",
+                /* content= */ "Gift document = Gift.create(idConv, namespaceConv, priceConv);");
+    }
+
+    @Test
+    // With golden class for factory method.
+    public void testWrite_bothUsableFactoryMethodAndConstructor_picksFirstUsableCreationMethod()
+            throws IOException {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  Gift(String id, String namespace, int price) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
+                        + "    this.price = price;\n"
+                        + "  }\n"
+                        + "  public static Gift create(String id, String namespace, int price) {\n"
+                        + "    return new Gift(id, namespace, price);"
+                        + "  }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.LongProperty int price;\n"
+                        + "}\n");
+
+        assertThat(compilation).succeededWithoutWarnings();
+        checkResultContains(/* className= */ "Gift.java",
+                /* content= */ "Gift document = new Gift(idConv, namespaceConv, priceConv);");
+    }
+
+    @Test
+    public void testWrite_usableFactoryMethod_unusableConstructor()
+            throws IOException {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  private Gift(String id, String namespace, int price) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
+                        + "    this.price = price;\n"
+                        + "  }\n"
+                        + "  public static Gift create(String id, String namespace, int price) {\n"
+                        + "    return new Gift(id, namespace, price);"
+                        + "  }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.LongProperty int price;\n"
+                        + "}\n");
+
+        assertThat(compilation).succeededWithoutWarnings();
+        checkResultContains(/* className= */ "Gift.java",
+                /* content= */ "Gift document = Gift.create(idConv, namespaceConv, priceConv);");
+        checkEqualsGolden("Gift.java");
+    }
+
+    @Test
+    public void testWrite_unusableFactoryMethod_usableConstructor()
+            throws IOException {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  Gift(String id, String namespace, int price) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
+                        + "    this.price = price;\n"
+                        + "  }\n"
+                        + "  private static Gift create(String id, String namespace, int price){\n"
+                        + "    return new Gift(id, namespace, price);"
+                        + "  }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.LongProperty int price;\n"
+                        + "}\n");
+
+        assertThat(compilation).succeededWithoutWarnings();
+        checkResultContains(/* className= */ "Gift.java",
+                /* content= */ "Gift document = new Gift(idConv, namespaceConv, priceConv);");
     }
 
     @Test
     public void testWrite_constructorExtraParams() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  Gift(int price, String uri, int unknownParam) {\n"
-                        + "    this.uri = uri;\n"
+                        + "  Gift(int price, String id, String namespace, int unknownParam) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
                         + "    this.price = price;\n"
                         + "  }\n"
-                        + "  @AppSearchDocument.Uri final String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.LongProperty int price;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Failed to find any suitable constructors to build this class");
-        CompilationSubject.assertThat(compilation).hadWarningContaining(
+
+        assertThat(compilation).hadErrorContaining(
+                "Failed to find any suitable creation methods to build this class");
+        assertThat(compilation).hadWarningContaining(
                 "Parameter \"unknownParam\" is not an AppSearch parameter; don't know how to "
                         + "supply it");
     }
 
     @Test
+    public void testWrite_multipleConventions() throws Exception {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id private String id_;\n"
+                        + "  @Document.LongProperty private int price1;\n"
+                        + "  @Document.LongProperty private int mPrice2;\n"
+                        + "  @Document.LongProperty private int _price3;\n"
+                        + "  @Document.LongProperty final int price4_;\n"
+                        + "  int getPrice1() { return price1; }\n"
+                        + "  int price2() { return mPrice2; }\n"
+                        + "  int getPrice3() { return _price3; }\n"
+                        + "  void setPrice1(int n) {}\n"
+                        + "  void price2(int n) {}\n"
+                        + "  void price3(int n) {}\n"
+                        + "  String getId() {\n"
+                        + "    return id_;\n"
+                        + "  }\n"
+                        + "  public Gift(String id, int price4) {\n"
+                        + "    id_ = id;"
+                        + "    price4_ = price4;\n"
+                        + "  }\n"
+                        + "}\n");
+        assertThat(compilation).succeededWithoutWarnings();
+        checkEqualsGolden("Gift.java");
+    }
+
+    @Test
     public void testSuccessSimple() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  Gift(boolean dog, String uri) {\n"
-                        + "    this.uri = uri;\n"
+                        + "  Gift(boolean dog, String id, String namespace) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
                         + "    this.dog = dog;\n"
                         + "  }\n"
-                        + "  @AppSearchDocument.Uri final String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
-                        + "  @AppSearchDocument.Property boolean cat = false;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.LongProperty int price;\n"
+                        + "  @Document.BooleanProperty boolean cat = false;\n"
                         + "  public void setCat(boolean cat) {}\n"
-                        + "  @AppSearchDocument.Property private final boolean dog;\n"
+                        + "  @Document.BooleanProperty private final boolean dog;\n"
                         + "  public boolean getDog() { return dog; }\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
     @Test
     public void testDifferentTypeName() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument(name=\"DifferentType\")\n"
+                "@Document(name=\"DifferentType\")\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -380,15 +583,17 @@
     public void testRepeatedFields() throws Exception {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property List<String> listOfString;\n"
-                        + "  @AppSearchDocument.Property Collection<Integer> setOfInt;\n"
-                        + "  @AppSearchDocument.Property byte[][] repeatedByteArray;\n"
-                        + "  @AppSearchDocument.Property byte[] byteArray;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty List<String> listOfString;\n"
+                        + "  @Document.LongProperty Collection<Integer> setOfInt;\n"
+                        + "  @Document.BytesProperty byte[][] repeatedByteArray;\n"
+                        + "  @Document.BytesProperty byte[] byteArray;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -396,15 +601,17 @@
     public void testCardinality() throws Exception {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(required=true) List<String> repeatReq;\n"
-                        + " @AppSearchDocument.Property(required=false) List<String> repeatNoReq;\n"
-                        + "  @AppSearchDocument.Property(required=true) Float req;\n"
-                        + "  @AppSearchDocument.Property(required=false) Float noReq;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty(required=true) List<String> repeatReq;\n"
+                        + "  @Document.StringProperty(required=false) List<String> repeatNoReq;\n"
+                        + "  @Document.DoubleProperty(required=true) Float req;\n"
+                        + "  @Document.DoubleProperty(required=false) Float noReq;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -413,19 +620,21 @@
         // TODO(b/156296904): Uncomment Gift in this test when it's supported
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property String stringProp;\n"
-                        + "  @AppSearchDocument.Property Integer integerProp;\n"
-                        + "  @AppSearchDocument.Property Long longProp;\n"
-                        + "  @AppSearchDocument.Property Float floatProp;\n"
-                        + "  @AppSearchDocument.Property Double doubleProp;\n"
-                        + "  @AppSearchDocument.Property Boolean booleanProp;\n"
-                        + "  @AppSearchDocument.Property byte[] bytesProp;\n"
-                        //+ "  @AppSearchDocument.Property Gift documentProp;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty String stringProp;\n"
+                        + "  @Document.LongProperty Integer integerProp;\n"
+                        + "  @Document.LongProperty Long longProp;\n"
+                        + "  @Document.DoubleProperty Float floatProp;\n"
+                        + "  @Document.DoubleProperty Double doubleProp;\n"
+                        + "  @Document.BooleanProperty Boolean booleanProp;\n"
+                        + "  @Document.BytesProperty byte[] bytesProp;\n"
+                        //+ "  @Document.Property Gift documentProp;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -435,13 +644,15 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(tokenizerType=0) String tokNone;\n"
-                        + "  @AppSearchDocument.Property(tokenizerType=1) String tokPlain;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty(tokenizerType=0) String tokNone;\n"
+                        + "  @Document.StringProperty(tokenizerType=1) String tokPlain;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -451,13 +662,15 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(indexingType=1, tokenizerType=100)\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty(indexingType=1, tokenizerType=100)\n"
                         + "  String str;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining("Unknown tokenizer type 100");
+
+        assertThat(compilation).hadErrorContaining("Unknown tokenizer type 100");
     }
 
     @Test
@@ -466,14 +679,16 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(indexingType=0) String indexNone;\n"
-                        + "  @AppSearchDocument.Property(indexingType=1) String indexExact;\n"
-                        + "  @AppSearchDocument.Property(indexingType=2) String indexPrefix;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty(indexingType=0) String indexNone;\n"
+                        + "  @Document.StringProperty(indexingType=1) String indexExact;\n"
+                        + "  @Document.StringProperty(indexingType=2) String indexPrefix;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -483,25 +698,29 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(indexingType=100, tokenizerType=1)\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty(indexingType=100, tokenizerType=1)\n"
                         + "  String str;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining("Unknown indexing type 100");
+
+        assertThat(compilation).hadErrorContaining("Unknown indexing type 100");
     }
 
     @Test
     public void testPropertyName() throws Exception {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(name=\"newName\") String oldName;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty(name=\"newName\") String oldName;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -511,146 +730,143 @@
         Compilation compilation = compile(
                 "import java.util.*;\n"
                         + "import androidx.appsearch.app.GenericDocument;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "\n"
                         + "  // Collections\n"
-                        + "  @Property Collection<Long> collectLong;\n"         // 1a
-                        + "  @Property Collection<Integer> collectInteger;\n"   // 1a
-                        + "  @Property Collection<Double> collectDouble;\n"     // 1a
-                        + "  @Property Collection<Float> collectFloat;\n"       // 1a
-                        + "  @Property Collection<Boolean> collectBoolean;\n"   // 1a
-                        + "  @Property Collection<byte[]> collectByteArr;\n"    // 1a
-                        + "  @Property Collection<String> collectString;\n"     // 1b
-                        + "  @Property Collection<Gift> collectGift;\n"         // 1c
+                        + "  @LongProperty Collection<Long> collectLong;\n"         // 1a
+                        + "  @LongProperty Collection<Integer> collectInteger;\n"   // 1a
+                        + "  @DoubleProperty Collection<Double> collectDouble;\n"     // 1a
+                        + "  @DoubleProperty Collection<Float> collectFloat;\n"       // 1a
+                        + "  @BooleanProperty Collection<Boolean> collectBoolean;\n"   // 1a
+                        + "  @BytesProperty Collection<byte[]> collectByteArr;\n"    // 1a
+                        + "  @StringProperty Collection<String> collectString;\n"     // 1b
+                        + "  @DocumentProperty Collection<Gift> collectGift;\n"         // 1c
                         + "\n"
                         + "  // Arrays\n"
-                        + "  @Property Long[] arrBoxLong;\n"         // 2a
-                        + "  @Property long[] arrUnboxLong;\n"       // 2b
-                        + "  @Property Integer[] arrBoxInteger;\n"   // 2a
-                        + "  @Property int[] arrUnboxInt;\n"         // 2a
-                        + "  @Property Double[] arrBoxDouble;\n"     // 2a
-                        + "  @Property double[] arrUnboxDouble;\n"   // 2b
-                        + "  @Property Float[] arrBoxFloat;\n"       // 2a
-                        + "  @Property float[] arrUnboxFloat;\n"     // 2a
-                        + "  @Property Boolean[] arrBoxBoolean;\n"   // 2a
-                        + "  @Property boolean[] arrUnboxBoolean;\n" // 2b
-                        + "  @Property byte[][] arrUnboxByteArr;\n"  // 2b
-                        + "  @Property Byte[] boxByteArr;\n"         // 2a
-                        + "  @Property String[] arrString;\n"        // 2b
-                        + "  @Property Gift[] arrGift;\n"            // 2c
+                        + "  @LongProperty Long[] arrBoxLong;\n"         // 2a
+                        + "  @LongProperty long[] arrUnboxLong;\n"       // 2b
+                        + "  @LongProperty Integer[] arrBoxInteger;\n"   // 2a
+                        + "  @LongProperty int[] arrUnboxInt;\n"         // 2a
+                        + "  @DoubleProperty Double[] arrBoxDouble;\n"     // 2a
+                        + "  @DoubleProperty double[] arrUnboxDouble;\n"   // 2b
+                        + "  @DoubleProperty Float[] arrBoxFloat;\n"       // 2a
+                        + "  @DoubleProperty float[] arrUnboxFloat;\n"     // 2a
+                        + "  @BooleanProperty Boolean[] arrBoxBoolean;\n"   // 2a
+                        + "  @BooleanProperty boolean[] arrUnboxBoolean;\n" // 2b
+                        + "  @BytesProperty byte[][] arrUnboxByteArr;\n"  // 2b
+                        + "  @BytesProperty Byte[] boxByteArr;\n"         // 2a
+                        + "  @StringProperty String[] arrString;\n"        // 2b
+                        + "  @DocumentProperty Gift[] arrGift;\n"            // 2c
                         + "\n"
                         + "  // Single values\n"
-                        + "  @Property String string;\n"        // 3a
-                        + "  @Property Long boxLong;\n"         // 3a
-                        + "  @Property long unboxLong;\n"       // 3b
-                        + "  @Property Integer boxInteger;\n"   // 3a
-                        + "  @Property int unboxInt;\n"         // 3b
-                        + "  @Property Double boxDouble;\n"     // 3a
-                        + "  @Property double unboxDouble;\n"   // 3b
-                        + "  @Property Float boxFloat;\n"       // 3a
-                        + "  @Property float unboxFloat;\n"     // 3b
-                        + "  @Property Boolean boxBoolean;\n"   // 3a
-                        + "  @Property boolean unboxBoolean;\n" // 3b
-                        + "  @Property byte[] unboxByteArr;\n"  // 3a
-                        + "  @Property Gift gift;\n"            // 3c
+                        + "  @StringProperty String string;\n"        // 3a
+                        + "  @LongProperty Long boxLong;\n"         // 3a
+                        + "  @LongProperty long unboxLong;\n"       // 3b
+                        + "  @LongProperty Integer boxInteger;\n"   // 3a
+                        + "  @LongProperty int unboxInt;\n"         // 3b
+                        + "  @DoubleProperty Double boxDouble;\n"     // 3a
+                        + "  @DoubleProperty double unboxDouble;\n"   // 3b
+                        + "  @DoubleProperty Float boxFloat;\n"       // 3a
+                        + "  @DoubleProperty float unboxFloat;\n"     // 3b
+                        + "  @BooleanProperty Boolean boxBoolean;\n"   // 3a
+                        + "  @BooleanProperty boolean unboxBoolean;\n" // 3b
+                        + "  @BytesProperty byte[] unboxByteArr;\n"  // 3a
+                        + "  @DocumentProperty Gift gift;\n"            // 3c
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
     @Test
+    public void testPropertyAnnotation_invalidType() {
+        Compilation compilation = compile(
+                "import java.util.*;\n"
+                        + "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
+                        + "  @BooleanProperty String[] arrString;\n"
+                        + "}\n");
+
+        assertThat(compilation).hadErrorContaining(
+                "Property Annotation androidx.appsearch.annotation.Document.BooleanProperty "
+                        + "doesn't accept the data type of property field arrString");
+    }
+
+    @Test
     public void testToGenericDocument_invalidTypes() {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
-                        + "  @Property Collection<Byte[]> collectBoxByteArr;\n" // 1x
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
+                        + "  @BytesProperty Collection<Byte[]> collectBoxByteArr;\n" // 1x
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+        assertThat(compilation).hadErrorContaining(
                 "Unhandled out property type (1x): java.util.Collection<java.lang.Byte[]>");
 
         compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
-                        + "  @Property Collection<Byte> collectByte;\n" // 1x
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
+                        + "  @BytesProperty Collection<Byte> collectByte;\n" // 1x
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+        assertThat(compilation).hadErrorContaining(
                 "Unhandled out property type (1x): java.util.Collection<java.lang.Byte>");
 
         compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
-                        + "  @Property Collection<Object> collectObject;\n" // 1x
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
+                        + "  @BytesProperty Byte[][] arrBoxByteArr;\n" // 2x
                         + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Unhandled out property type (1x): java.util.Collection<java.lang.Object>");
-
-        compilation = compile(
-                "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
-                        + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
-                        + "  @Property Byte[][] arrBoxByteArr;\n" // 2x
-                        + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
+        assertThat(compilation).hadErrorContaining(
                 "Unhandled out property type (2x): java.lang.Byte[][]");
-
-        compilation = compile(
-                "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
-                        + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
-                        + "  @Property Object[] arrObject;\n" // 2x
-                        + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Unhandled out property type (2x): java.lang.Object[]");
-
-        compilation = compile(
-                "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
-                        + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
-                        + "  @Property Object object;\n" // 3x
-                        + "}\n");
-        CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Unhandled out property type (3x): java.lang.Object");
     }
 
     @Test
     public void testAllSpecialFields_field() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Namespace String namespace;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis long creationTs;\n"
-                        + "  @AppSearchDocument.TtlMillis int ttlMs;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
-                        + "  @AppSearchDocument.Score int score;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.CreationTimestampMillis long creationTs;\n"
+                        + "  @Document.TtlMillis int ttlMs;\n"
+                        + "  @Document.LongProperty int price;\n"
+                        + "  @Document.Score int score;\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
     @Test
     public void testAllSpecialFields_getter() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri private String uri;\n"
-                        + "  @AppSearchDocument.Score private int score;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis private long creationTs;\n"
-                        + "  @AppSearchDocument.TtlMillis private int ttlMs;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
-                        + "  public String getUri() { return uri; }\n"
-                        + "  public void setUri(String uri) { this.uri = uri; }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id private String id;\n"
+                        + "  @Document.Score private int score;\n"
+                        + "  @Document.CreationTimestampMillis private long creationTs;\n"
+                        + "  @Document.TtlMillis private int ttlMs;\n"
+                        + "  @Document.LongProperty private int price;\n"
+                        + "  public String getId() { return id; }\n"
+                        + "  public void setId(String id) { this.id = id; }\n"
+                        + "  public String getNamespace() { return namespace; }\n"
+                        + "  public void setNamespace(String namespace) {\n"
+                        + "    this.namespace = namespace;\n"
+                        + "  }\n"
                         + "  public int getScore() { return score; }\n"
                         + "  public void setScore(int score) { this.score = score; }\n"
                         + "  public long getCreationTs() { return creationTs; }\n"
@@ -662,7 +878,30 @@
                         + "  public int getPrice() { return price; }\n"
                         + "  public void setPrice(int price) { this.price = price; }\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
+        checkEqualsGolden("Gift.java");
+    }
+
+    @Test
+    public void testAutoValueDocument() throws IOException {
+        Compilation compilation = compile(
+                "import com.google.auto.value.AutoValue;\n"
+                        + "import com.google.auto.value.AutoValue.*;\n"
+                        + "@Document\n"
+                        + "@AutoValue\n"
+                        + "public abstract class Gift {\n"
+                        + "  @CopyAnnotations @Document.Id abstract String id();\n"
+                        + "  @CopyAnnotations @Document.Namespace abstract String namespace();\n"
+                        + "  @CopyAnnotations\n"
+                        + "  @Document.StringProperty abstract String property();\n"
+                        + "  public static Gift create(String id, String namespace, String"
+                        + " property) {\n"
+                        + "    return new AutoValue_Gift(id, namespace, property);\n"
+                        + "  }\n"
+                        + "}\n");
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
     }
 
@@ -672,13 +911,15 @@
                 "import java.util.*;\n"
                         + "import androidx.appsearch.app.GenericDocument;\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument\n"
+                        + "  @Document\n"
                         + "  public static class InnerGift{\n"
-                        + "    @AppSearchDocument.Uri String uri;\n"
-                        + "    @Property String[] arrString;\n"        // 2b
+                        + "    @Document.Namespace String namespace;\n"
+                        + "    @Document.Id String id;\n"
+                        + "    @StringProperty String[] arrString;\n"        // 2b
                         + "  }\n"
                         + "}\n");
-        CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
+
+        assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift$$__InnerGift.java");
     }
 
@@ -688,8 +929,8 @@
 
     private Compilation compile(String classSimpleName, String classBody) {
         String src = "package com.example.appsearch;\n"
-                + "import androidx.appsearch.annotation.AppSearchDocument;\n"
-                + "import androidx.appsearch.annotation.AppSearchDocument.*;\n"
+                + "import androidx.appsearch.annotation.Document;\n"
+                + "import androidx.appsearch.annotation.Document.*;\n"
                 + classBody;
         JavaFileObject jfo = JavaFileObjects.forSourceString(
                 "com.example.appsearch." + classSimpleName,
@@ -702,7 +943,7 @@
                 AppSearchCompiler.OUTPUT_DIR_OPTION,
                 mGenFilesDir.getAbsolutePath());
         return Compiler.javac()
-                .withProcessors(new AppSearchCompiler())
+                .withProcessors(new AppSearchCompiler(), new AutoValueProcessor())
                 .withOptions(outputDirFlag)
                 .compile(jfo);
     }
@@ -722,7 +963,8 @@
 
         // Get the actual file contents
         File actualPackageDir = new File(mGenFilesDir, "com/example/appsearch");
-        File actualPath = new File(actualPackageDir, CodeGenerator.GEN_CLASS_PREFIX + className);
+        File actualPath =
+                new File(actualPackageDir, IntrospectionHelper.GEN_CLASS_PREFIX + className);
         Truth.assertWithMessage("Path " + actualPath + " is not a file")
                 .that(actualPath.isFile()).isTrue();
         String actual = Files.asCharSource(actualPath, StandardCharsets.UTF_8).read();
@@ -751,4 +993,16 @@
             Truth.assertThat(actual).isEqualTo(expected);
         }
     }
+
+    private void checkResultContains(String className, String content) throws IOException {
+        // Get the actual file contents
+        File actualPackageDir = new File(mGenFilesDir, "com/example/appsearch");
+        File actualPath =
+                new File(actualPackageDir, IntrospectionHelper.GEN_CLASS_PREFIX + className);
+        Truth.assertWithMessage("Path " + actualPath + " is not a file")
+                .that(actualPath.isFile()).isTrue();
+        String actual = Files.asCharSource(actualPath, StandardCharsets.UTF_8).read();
+
+        Truth.assertThat(actual).contains(content);
+    }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
index b256fa5..af0b990 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
@@ -1,7 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Boolean;
@@ -11,92 +11,75 @@
 import java.lang.Long;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("stringProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("stringProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("integerProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("integerProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("longProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("longProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("floatProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("floatProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("doubleProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("doubleProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("booleanProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("bytesProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytesProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String stringPropCopy = dataClass.stringProp;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String stringPropCopy = document.stringProp;
     if (stringPropCopy != null) {
       builder.setPropertyString("stringProp", stringPropCopy);
     }
-    Integer integerPropCopy = dataClass.integerProp;
+    Integer integerPropCopy = document.integerProp;
     if (integerPropCopy != null) {
       builder.setPropertyLong("integerProp", integerPropCopy);
     }
-    Long longPropCopy = dataClass.longProp;
+    Long longPropCopy = document.longProp;
     if (longPropCopy != null) {
       builder.setPropertyLong("longProp", longPropCopy);
     }
-    Float floatPropCopy = dataClass.floatProp;
+    Float floatPropCopy = document.floatProp;
     if (floatPropCopy != null) {
       builder.setPropertyDouble("floatProp", floatPropCopy);
     }
-    Double doublePropCopy = dataClass.doubleProp;
+    Double doublePropCopy = document.doubleProp;
     if (doublePropCopy != null) {
       builder.setPropertyDouble("doubleProp", doublePropCopy);
     }
-    Boolean booleanPropCopy = dataClass.booleanProp;
+    Boolean booleanPropCopy = document.booleanProp;
     if (booleanPropCopy != null) {
       builder.setPropertyBoolean("booleanProp", booleanPropCopy);
     }
-    byte[] bytesPropCopy = dataClass.bytesProp;
+    byte[] bytesPropCopy = document.bytesProp;
     if (bytesPropCopy != null) {
       builder.setPropertyBytes("bytesProp", bytesPropCopy);
     }
@@ -105,7 +88,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] stringPropCopy = genericDoc.getPropertyStringArray("stringProp");
     String stringPropConv = null;
     if (stringPropCopy != null && stringPropCopy.length != 0) {
@@ -141,15 +125,16 @@
     if (bytesPropCopy != null && bytesPropCopy.length != 0) {
       bytesPropConv = bytesPropCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.stringProp = stringPropConv;
-    dataClass.integerProp = integerPropConv;
-    dataClass.longProp = longPropConv;
-    dataClass.floatProp = floatPropConv;
-    dataClass.doubleProp = doublePropConv;
-    dataClass.booleanProp = booleanPropConv;
-    dataClass.bytesProp = bytesPropConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.stringProp = stringPropConv;
+    document.integerProp = integerPropConv;
+    document.longProp = longPropConv;
+    document.floatProp = floatPropConv;
+    document.doubleProp = doublePropConv;
+    document.booleanProp = booleanPropConv;
+    document.bytesProp = bytesPropConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
index e315345..0824d2b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
@@ -1,62 +1,57 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String namespaceCopy = dataClass.namespace;
-    if (namespaceCopy != null) {
-      builder.setNamespace(namespaceCopy);
-    }
-    builder.setCreationTimestampMillis(dataClass.creationTs);
-    builder.setTtlMillis(dataClass.ttlMs);
-    builder.setScore(dataClass.score);
-    builder.setPropertyLong("price", dataClass.price);
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setCreationTimestampMillis(document.creationTs);
+    builder.setTtlMillis(document.ttlMs);
+    builder.setScore(document.score);
+    builder.setPropertyLong("price", document.price);
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
     String namespaceConv = genericDoc.getNamespace();
     long creationTsConv = genericDoc.getCreationTimestampMillis();
     long ttlMsConv = genericDoc.getTtlMillis();
     int scoreConv = genericDoc.getScore();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.namespace = namespaceConv;
-    dataClass.creationTs = creationTsConv;
-    dataClass.ttlMs = ttlMsConv;
-    dataClass.price = priceConv;
-    dataClass.score = scoreConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.creationTs = creationTsConv;
+    document.ttlMs = ttlMsConv;
+    document.price = priceConv;
+    document.score = scoreConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
index 1cf9253..aa6366c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
@@ -1,56 +1,57 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.getUri(), SCHEMA_TYPE);
-    builder.setCreationTimestampMillis(dataClass.getCreationTs());
-    builder.setTtlMillis(dataClass.getTtlMs());
-    builder.setScore(dataClass.getScore());
-    builder.setPropertyLong("price", dataClass.getPrice());
+        new GenericDocument.Builder<>(document.namespace, document.getId(), SCHEMA_NAME);
+    builder.setCreationTimestampMillis(document.getCreationTs());
+    builder.setTtlMillis(document.getTtlMs());
+    builder.setScore(document.getScore());
+    builder.setPropertyLong("price", document.getPrice());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     long creationTsConv = genericDoc.getCreationTimestampMillis();
     long ttlMsConv = genericDoc.getTtlMillis();
     int scoreConv = genericDoc.getScore();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.setUri(uriConv);
-    dataClass.setScore(scoreConv);
-    dataClass.setCreationTs(creationTsConv);
-    dataClass.setTtlMs(ttlMsConv);
-    dataClass.setPrice(priceConv);
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.setId(idConv);
+    document.setScore(scoreConv);
+    document.setCreationTs(creationTsConv);
+    document.setTtlMs(ttlMsConv);
+    document.setPrice(priceConv);
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
new file mode 100644
index 0000000..37638c3
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocument.JAVA
@@ -0,0 +1,54 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Override;
+import java.lang.String;
+import javax.annotation.Generated;
+
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
+
+  @Override
+  public String getSchemaName() {
+    return SCHEMA_NAME;
+  }
+
+  @Override
+  public AppSearchSchema getSchema() throws AppSearchException {
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("property")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
+          .build();
+  }
+
+  @Override
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+    GenericDocument.Builder<?> builder =
+        new GenericDocument.Builder<>(document.namespace(), document.id(), SCHEMA_NAME);
+    String propertyCopy = document.property();
+    if (propertyCopy != null) {
+      builder.setPropertyString("property", propertyCopy);
+    }
+    return builder.build();
+  }
+
+  @Override
+  public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
+    String[] propertyCopy = genericDoc.getPropertyStringArray("property");
+    String propertyConv = null;
+    if (propertyCopy != null && propertyCopy.length != 0) {
+      propertyConv = propertyCopy[0];
+    }
+    Gift document = Gift.create(idConv, namespaceConv, propertyConv);
+    return document;
+  }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
index 13f4f18..0fa2028 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
@@ -1,7 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Float;
@@ -9,64 +9,58 @@
 import java.lang.String;
 import java.util.Arrays;
 import java.util.List;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatReq")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("repeatReq")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatNoReq")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("repeatNoReq")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("req")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("req")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("noReq")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("noReq")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    List<String> repeatReqCopy = dataClass.repeatReq;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    List<String> repeatReqCopy = document.repeatReq;
     if (repeatReqCopy != null) {
       String[] repeatReqConv = repeatReqCopy.toArray(new String[0]);
       builder.setPropertyString("repeatReq", repeatReqConv);
     }
-    List<String> repeatNoReqCopy = dataClass.repeatNoReq;
+    List<String> repeatNoReqCopy = document.repeatNoReq;
     if (repeatNoReqCopy != null) {
       String[] repeatNoReqConv = repeatNoReqCopy.toArray(new String[0]);
       builder.setPropertyString("repeatNoReq", repeatNoReqConv);
     }
-    Float reqCopy = dataClass.req;
+    Float reqCopy = document.req;
     if (reqCopy != null) {
       builder.setPropertyDouble("req", reqCopy);
     }
-    Float noReqCopy = dataClass.noReq;
+    Float noReqCopy = document.noReq;
     if (noReqCopy != null) {
       builder.setPropertyDouble("noReq", noReqCopy);
     }
@@ -75,7 +69,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] repeatReqCopy = genericDoc.getPropertyStringArray("repeatReq");
     List<String> repeatReqConv = null;
     if (repeatReqCopy != null) {
@@ -96,12 +91,13 @@
     if (noReqCopy != null && noReqCopy.length != 0) {
       noReqConv = (float) noReqCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.repeatReq = repeatReqConv;
-    dataClass.repeatNoReq = repeatNoReqConv;
-    dataClass.req = reqConv;
-    dataClass.noReq = noReqConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.repeatReq = repeatReqConv;
+    document.repeatNoReq = repeatNoReqConv;
+    document.req = reqConv;
+    document.noReq = noReqConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
index 2c55500..27247d7 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
@@ -1,38 +1,42 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "DifferentType";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "DifferentType";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    return dataClass;
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
new file mode 100644
index 0000000..f3c0f3d
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testGetterAndSetterFunctions_withFieldName.JAVA
@@ -0,0 +1,48 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Override;
+import java.lang.String;
+import javax.annotation.Generated;
+
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
+
+  @Override
+  public String getSchemaName() {
+    return SCHEMA_NAME;
+  }
+
+  @Override
+  public AppSearchSchema getSchema() throws AppSearchException {
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .build())
+          .build();
+  }
+
+  @Override
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+    GenericDocument.Builder<?> builder =
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.price());
+    return builder.build();
+  }
+
+  @Override
+  public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
+    int priceConv = (int) genericDoc.getPropertyLong("price");
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.price(priceConv);
+    return document;
+  }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
index 87283d4..0e4a18f 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
@@ -1,57 +1,56 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexNone")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("indexNone")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexExact")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("indexExact")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexPrefix")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("indexPrefix")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String indexNoneCopy = dataClass.indexNone;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String indexNoneCopy = document.indexNone;
     if (indexNoneCopy != null) {
       builder.setPropertyString("indexNone", indexNoneCopy);
     }
-    String indexExactCopy = dataClass.indexExact;
+    String indexExactCopy = document.indexExact;
     if (indexExactCopy != null) {
       builder.setPropertyString("indexExact", indexExactCopy);
     }
-    String indexPrefixCopy = dataClass.indexPrefix;
+    String indexPrefixCopy = document.indexPrefix;
     if (indexPrefixCopy != null) {
       builder.setPropertyString("indexPrefix", indexPrefixCopy);
     }
@@ -60,7 +59,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] indexNoneCopy = genericDoc.getPropertyStringArray("indexNone");
     String indexNoneConv = null;
     if (indexNoneCopy != null && indexNoneCopy.length != 0) {
@@ -76,11 +76,12 @@
     if (indexPrefixCopy != null && indexPrefixCopy.length != 0) {
       indexPrefixConv = indexPrefixCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.indexNone = indexNoneConv;
-    dataClass.indexExact = indexExactConv;
-    dataClass.indexPrefix = indexPrefixConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.indexNone = indexNoneConv;
+    document.indexExact = indexExactConv;
+    document.indexPrefix = indexPrefixConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
index 8900092..37ada00 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
@@ -1,37 +1,38 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift$$__InnerGift implements DataClassFactory<Gift.InnerGift> {
-  private static final String SCHEMA_TYPE = "InnerGift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift$$__InnerGift implements DocumentClassFactory<Gift.InnerGift> {
+  public static final String SCHEMA_NAME = "InnerGift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("arrString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift.InnerGift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift.InnerGift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String[] arrStringCopy = dataClass.arrString;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String[] arrStringCopy = document.arrString;
     if (arrStringCopy != null) {
       builder.setPropertyString("arrString", arrStringCopy);
     }
@@ -40,11 +41,13 @@
 
   @Override
   public Gift.InnerGift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] arrStringConv = genericDoc.getPropertyStringArray("arrString");
-    Gift.InnerGift dataClass = new Gift.InnerGift();
-    dataClass.uri = uriConv;
-    dataClass.arrString = arrStringConv;
-    return dataClass;
+    Gift.InnerGift document = new Gift.InnerGift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.arrString = arrStringConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
index d843153..5aaf27b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
@@ -1,37 +1,38 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("newName")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("newName")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String oldNameCopy = dataClass.oldName;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String oldNameCopy = document.oldName;
     if (oldNameCopy != null) {
       builder.setPropertyString("newName", oldNameCopy);
     }
@@ -40,15 +41,17 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] oldNameCopy = genericDoc.getPropertyStringArray("newName");
     String oldNameConv = null;
     if (oldNameCopy != null && oldNameCopy.length != 0) {
       oldNameConv = oldNameCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.oldName = oldNameConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.oldName = oldNameConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
index 890d43d..9c71ee4 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
@@ -1,47 +1,48 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    builder.setPropertyLong("price", dataClass.getPrice());
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.getPrice());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.setPrice(priceConv);
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.setPrice(priceConv);
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
index be28a52..b4d51d5 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
@@ -1,7 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Integer;
@@ -11,55 +11,47 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("listOfString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("listOfString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("setOfInt")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("setOfInt")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatedByteArray")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("repeatedByteArray")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("byteArray")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("byteArray")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    List<String> listOfStringCopy = dataClass.listOfString;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    List<String> listOfStringCopy = document.listOfString;
     if (listOfStringCopy != null) {
       String[] listOfStringConv = listOfStringCopy.toArray(new String[0]);
       builder.setPropertyString("listOfString", listOfStringConv);
     }
-    Collection<Integer> setOfIntCopy = dataClass.setOfInt;
+    Collection<Integer> setOfIntCopy = document.setOfInt;
     if (setOfIntCopy != null) {
       long[] setOfIntConv = new long[setOfIntCopy.size()];
       int i = 0;
@@ -68,11 +60,11 @@
       }
       builder.setPropertyLong("setOfInt", setOfIntConv);
     }
-    byte[][] repeatedByteArrayCopy = dataClass.repeatedByteArray;
+    byte[][] repeatedByteArrayCopy = document.repeatedByteArray;
     if (repeatedByteArrayCopy != null) {
       builder.setPropertyBytes("repeatedByteArray", repeatedByteArrayCopy);
     }
-    byte[] byteArrayCopy = dataClass.byteArray;
+    byte[] byteArrayCopy = document.byteArray;
     if (byteArrayCopy != null) {
       builder.setPropertyBytes("byteArray", byteArrayCopy);
     }
@@ -81,7 +73,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] listOfStringCopy = genericDoc.getPropertyStringArray("listOfString");
     List<String> listOfStringConv = null;
     if (listOfStringCopy != null) {
@@ -101,12 +94,13 @@
     if (byteArrayCopy != null && byteArrayCopy.length != 0) {
       byteArrayConv = byteArrayCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.listOfString = listOfStringConv;
-    dataClass.setOfInt = setOfIntConv;
-    dataClass.repeatedByteArray = repeatedByteArrayConv;
-    dataClass.byteArray = byteArrayConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.listOfString = listOfStringConv;
+    document.setOfInt = setOfIntConv;
+    document.repeatedByteArray = repeatedByteArrayConv;
+    document.byteArray = byteArrayConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
index 8499df6..ee687ea 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
@@ -1,63 +1,58 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("cat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("cat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("dog")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("dog")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    builder.setPropertyLong("price", dataClass.price);
-    builder.setPropertyBoolean("cat", dataClass.cat);
-    builder.setPropertyBoolean("dog", dataClass.getDog());
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.price);
+    builder.setPropertyBoolean("cat", document.cat);
+    builder.setPropertyBoolean("dog", document.getDog());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     int priceConv = (int) genericDoc.getPropertyLong("price");
     boolean catConv = genericDoc.getPropertyBoolean("cat");
     boolean dogConv = genericDoc.getPropertyBoolean("dog");
-    Gift dataClass = new Gift(dogConv, uriConv);
-    dataClass.price = priceConv;
-    dataClass.cat = catConv;
-    return dataClass;
+    Gift document = new Gift(dogConv, idConv, namespaceConv);
+    document.namespace = namespaceConv;
+    document.price = priceConv;
+    document.cat = catConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
index d3ee29b..dc9e925 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
@@ -1,8 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
-import androidx.appsearch.app.DataClassFactoryRegistry;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Boolean;
@@ -17,239 +16,139 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("collectLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectInteger")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("collectInteger")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("collectDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("collectFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("collectBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("collectByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("collectString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectGift")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT)
-            .setSchemaType(DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).getSchemaType())
+          .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("collectGift", $$__AppSearch__Gift.SCHEMA_NAME)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("arrBoxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("arrUnboxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxInteger")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("arrBoxInteger")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxInt")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("arrUnboxInt")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrBoxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrUnboxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrBoxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrUnboxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("arrBoxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("arrUnboxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("arrUnboxByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("boxByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("arrString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrGift")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT)
-            .setSchemaType(DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).getSchemaType())
+          .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("arrGift", $$__AppSearch__Gift.SCHEMA_NAME)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("string")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("string")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("boxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("unboxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxInteger")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("boxInteger")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxInt")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("unboxInt")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("boxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("unboxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("boxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("unboxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("unboxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("unboxByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("gift")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT)
-            .setSchemaType(DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).getSchemaType())
+          .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("gift", $$__AppSearch__Gift.SCHEMA_NAME)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    Collection<Long> collectLongCopy = dataClass.collectLong;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    Collection<Long> collectLongCopy = document.collectLong;
     if (collectLongCopy != null) {
       long[] collectLongConv = new long[collectLongCopy.size()];
       int i = 0;
@@ -258,7 +157,7 @@
       }
       builder.setPropertyLong("collectLong", collectLongConv);
     }
-    Collection<Integer> collectIntegerCopy = dataClass.collectInteger;
+    Collection<Integer> collectIntegerCopy = document.collectInteger;
     if (collectIntegerCopy != null) {
       long[] collectIntegerConv = new long[collectIntegerCopy.size()];
       int i = 0;
@@ -267,7 +166,7 @@
       }
       builder.setPropertyLong("collectInteger", collectIntegerConv);
     }
-    Collection<Double> collectDoubleCopy = dataClass.collectDouble;
+    Collection<Double> collectDoubleCopy = document.collectDouble;
     if (collectDoubleCopy != null) {
       double[] collectDoubleConv = new double[collectDoubleCopy.size()];
       int i = 0;
@@ -276,7 +175,7 @@
       }
       builder.setPropertyDouble("collectDouble", collectDoubleConv);
     }
-    Collection<Float> collectFloatCopy = dataClass.collectFloat;
+    Collection<Float> collectFloatCopy = document.collectFloat;
     if (collectFloatCopy != null) {
       double[] collectFloatConv = new double[collectFloatCopy.size()];
       int i = 0;
@@ -285,7 +184,7 @@
       }
       builder.setPropertyDouble("collectFloat", collectFloatConv);
     }
-    Collection<Boolean> collectBooleanCopy = dataClass.collectBoolean;
+    Collection<Boolean> collectBooleanCopy = document.collectBoolean;
     if (collectBooleanCopy != null) {
       boolean[] collectBooleanConv = new boolean[collectBooleanCopy.size()];
       int i = 0;
@@ -294,7 +193,7 @@
       }
       builder.setPropertyBoolean("collectBoolean", collectBooleanConv);
     }
-    Collection<byte[]> collectByteArrCopy = dataClass.collectByteArr;
+    Collection<byte[]> collectByteArrCopy = document.collectByteArr;
     if (collectByteArrCopy != null) {
       byte[][] collectByteArrConv = new byte[collectByteArrCopy.size()][];
       int i = 0;
@@ -303,22 +202,21 @@
       }
       builder.setPropertyBytes("collectByteArr", collectByteArrConv);
     }
-    Collection<String> collectStringCopy = dataClass.collectString;
+    Collection<String> collectStringCopy = document.collectString;
     if (collectStringCopy != null) {
       String[] collectStringConv = collectStringCopy.toArray(new String[0]);
       builder.setPropertyString("collectString", collectStringConv);
     }
-    Collection<Gift> collectGiftCopy = dataClass.collectGift;
+    Collection<Gift> collectGiftCopy = document.collectGift;
     if (collectGiftCopy != null) {
       GenericDocument[] collectGiftConv = new GenericDocument[collectGiftCopy.size()];
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       int i = 0;
       for (Gift item : collectGiftCopy) {
-        collectGiftConv[i++] = factory.toGenericDocument(item);
+        collectGiftConv[i++] = GenericDocument.fromDocumentClass(item);
       }
       builder.setPropertyDocument("collectGift", collectGiftConv);
     }
-    Long[] arrBoxLongCopy = dataClass.arrBoxLong;
+    Long[] arrBoxLongCopy = document.arrBoxLong;
     if (arrBoxLongCopy != null) {
       long[] arrBoxLongConv = new long[arrBoxLongCopy.length];
       for (int i = 0 ; i < arrBoxLongCopy.length ; i++) {
@@ -326,11 +224,11 @@
       }
       builder.setPropertyLong("arrBoxLong", arrBoxLongConv);
     }
-    long[] arrUnboxLongCopy = dataClass.arrUnboxLong;
+    long[] arrUnboxLongCopy = document.arrUnboxLong;
     if (arrUnboxLongCopy != null) {
       builder.setPropertyLong("arrUnboxLong", arrUnboxLongCopy);
     }
-    Integer[] arrBoxIntegerCopy = dataClass.arrBoxInteger;
+    Integer[] arrBoxIntegerCopy = document.arrBoxInteger;
     if (arrBoxIntegerCopy != null) {
       long[] arrBoxIntegerConv = new long[arrBoxIntegerCopy.length];
       for (int i = 0 ; i < arrBoxIntegerCopy.length ; i++) {
@@ -338,7 +236,7 @@
       }
       builder.setPropertyLong("arrBoxInteger", arrBoxIntegerConv);
     }
-    int[] arrUnboxIntCopy = dataClass.arrUnboxInt;
+    int[] arrUnboxIntCopy = document.arrUnboxInt;
     if (arrUnboxIntCopy != null) {
       long[] arrUnboxIntConv = new long[arrUnboxIntCopy.length];
       for (int i = 0 ; i < arrUnboxIntCopy.length ; i++) {
@@ -346,7 +244,7 @@
       }
       builder.setPropertyLong("arrUnboxInt", arrUnboxIntConv);
     }
-    Double[] arrBoxDoubleCopy = dataClass.arrBoxDouble;
+    Double[] arrBoxDoubleCopy = document.arrBoxDouble;
     if (arrBoxDoubleCopy != null) {
       double[] arrBoxDoubleConv = new double[arrBoxDoubleCopy.length];
       for (int i = 0 ; i < arrBoxDoubleCopy.length ; i++) {
@@ -354,11 +252,11 @@
       }
       builder.setPropertyDouble("arrBoxDouble", arrBoxDoubleConv);
     }
-    double[] arrUnboxDoubleCopy = dataClass.arrUnboxDouble;
+    double[] arrUnboxDoubleCopy = document.arrUnboxDouble;
     if (arrUnboxDoubleCopy != null) {
       builder.setPropertyDouble("arrUnboxDouble", arrUnboxDoubleCopy);
     }
-    Float[] arrBoxFloatCopy = dataClass.arrBoxFloat;
+    Float[] arrBoxFloatCopy = document.arrBoxFloat;
     if (arrBoxFloatCopy != null) {
       double[] arrBoxFloatConv = new double[arrBoxFloatCopy.length];
       for (int i = 0 ; i < arrBoxFloatCopy.length ; i++) {
@@ -366,7 +264,7 @@
       }
       builder.setPropertyDouble("arrBoxFloat", arrBoxFloatConv);
     }
-    float[] arrUnboxFloatCopy = dataClass.arrUnboxFloat;
+    float[] arrUnboxFloatCopy = document.arrUnboxFloat;
     if (arrUnboxFloatCopy != null) {
       double[] arrUnboxFloatConv = new double[arrUnboxFloatCopy.length];
       for (int i = 0 ; i < arrUnboxFloatCopy.length ; i++) {
@@ -374,7 +272,7 @@
       }
       builder.setPropertyDouble("arrUnboxFloat", arrUnboxFloatConv);
     }
-    Boolean[] arrBoxBooleanCopy = dataClass.arrBoxBoolean;
+    Boolean[] arrBoxBooleanCopy = document.arrBoxBoolean;
     if (arrBoxBooleanCopy != null) {
       boolean[] arrBoxBooleanConv = new boolean[arrBoxBooleanCopy.length];
       for (int i = 0 ; i < arrBoxBooleanCopy.length ; i++) {
@@ -382,15 +280,15 @@
       }
       builder.setPropertyBoolean("arrBoxBoolean", arrBoxBooleanConv);
     }
-    boolean[] arrUnboxBooleanCopy = dataClass.arrUnboxBoolean;
+    boolean[] arrUnboxBooleanCopy = document.arrUnboxBoolean;
     if (arrUnboxBooleanCopy != null) {
       builder.setPropertyBoolean("arrUnboxBoolean", arrUnboxBooleanCopy);
     }
-    byte[][] arrUnboxByteArrCopy = dataClass.arrUnboxByteArr;
+    byte[][] arrUnboxByteArrCopy = document.arrUnboxByteArr;
     if (arrUnboxByteArrCopy != null) {
       builder.setPropertyBytes("arrUnboxByteArr", arrUnboxByteArrCopy);
     }
-    Byte[] boxByteArrCopy = dataClass.boxByteArr;
+    Byte[] boxByteArrCopy = document.boxByteArr;
     if (boxByteArrCopy != null) {
       byte[] boxByteArrConv = new byte[boxByteArrCopy.length];
       for (int i = 0 ; i < boxByteArrCopy.length ; i++) {
@@ -398,55 +296,54 @@
       }
       builder.setPropertyBytes("boxByteArr", boxByteArrConv);
     }
-    String[] arrStringCopy = dataClass.arrString;
+    String[] arrStringCopy = document.arrString;
     if (arrStringCopy != null) {
       builder.setPropertyString("arrString", arrStringCopy);
     }
-    Gift[] arrGiftCopy = dataClass.arrGift;
+    Gift[] arrGiftCopy = document.arrGift;
     if (arrGiftCopy != null) {
       GenericDocument[] arrGiftConv = new GenericDocument[arrGiftCopy.length];
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       for (int i = 0; i < arrGiftConv.length; i++) {
-        arrGiftConv[i] = factory.toGenericDocument(arrGiftCopy[i]);
+        arrGiftConv[i] = GenericDocument.fromDocumentClass(arrGiftCopy[i]);
       }
       builder.setPropertyDocument("arrGift", arrGiftConv);
     }
-    String stringCopy = dataClass.string;
+    String stringCopy = document.string;
     if (stringCopy != null) {
       builder.setPropertyString("string", stringCopy);
     }
-    Long boxLongCopy = dataClass.boxLong;
+    Long boxLongCopy = document.boxLong;
     if (boxLongCopy != null) {
       builder.setPropertyLong("boxLong", boxLongCopy);
     }
-    builder.setPropertyLong("unboxLong", dataClass.unboxLong);
-    Integer boxIntegerCopy = dataClass.boxInteger;
+    builder.setPropertyLong("unboxLong", document.unboxLong);
+    Integer boxIntegerCopy = document.boxInteger;
     if (boxIntegerCopy != null) {
       builder.setPropertyLong("boxInteger", boxIntegerCopy);
     }
-    builder.setPropertyLong("unboxInt", dataClass.unboxInt);
-    Double boxDoubleCopy = dataClass.boxDouble;
+    builder.setPropertyLong("unboxInt", document.unboxInt);
+    Double boxDoubleCopy = document.boxDouble;
     if (boxDoubleCopy != null) {
       builder.setPropertyDouble("boxDouble", boxDoubleCopy);
     }
-    builder.setPropertyDouble("unboxDouble", dataClass.unboxDouble);
-    Float boxFloatCopy = dataClass.boxFloat;
+    builder.setPropertyDouble("unboxDouble", document.unboxDouble);
+    Float boxFloatCopy = document.boxFloat;
     if (boxFloatCopy != null) {
       builder.setPropertyDouble("boxFloat", boxFloatCopy);
     }
-    builder.setPropertyDouble("unboxFloat", dataClass.unboxFloat);
-    Boolean boxBooleanCopy = dataClass.boxBoolean;
+    builder.setPropertyDouble("unboxFloat", document.unboxFloat);
+    Boolean boxBooleanCopy = document.boxBoolean;
     if (boxBooleanCopy != null) {
       builder.setPropertyBoolean("boxBoolean", boxBooleanCopy);
     }
-    builder.setPropertyBoolean("unboxBoolean", dataClass.unboxBoolean);
-    byte[] unboxByteArrCopy = dataClass.unboxByteArr;
+    builder.setPropertyBoolean("unboxBoolean", document.unboxBoolean);
+    byte[] unboxByteArrCopy = document.unboxByteArr;
     if (unboxByteArrCopy != null) {
       builder.setPropertyBytes("unboxByteArr", unboxByteArrCopy);
     }
-    Gift giftCopy = dataClass.gift;
+    Gift giftCopy = document.gift;
     if (giftCopy != null) {
-      GenericDocument giftConv = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).toGenericDocument(giftCopy);
+      GenericDocument giftConv = GenericDocument.fromDocumentClass(giftCopy);
       builder.setPropertyDocument("gift", giftConv);
     }
     return builder.build();
@@ -454,7 +351,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     long[] collectLongCopy = genericDoc.getPropertyLongArray("collectLong");
     List<Long> collectLongConv = null;
     if (collectLongCopy != null) {
@@ -511,10 +409,9 @@
     GenericDocument[] collectGiftCopy = genericDoc.getPropertyDocumentArray("collectGift");
     List<Gift> collectGiftConv = null;
     if (collectGiftCopy != null) {
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       collectGiftConv = new ArrayList<>(collectGiftCopy.length);
       for (int i = 0; i < collectGiftCopy.length; i++) {
-        collectGiftConv.add(factory.fromGenericDocument(collectGiftCopy[i]));
+        collectGiftConv.add(collectGiftCopy[i].toDocumentClass(Gift.class));
       }
     }
     long[] arrBoxLongCopy = genericDoc.getPropertyLongArray("arrBoxLong");
@@ -590,9 +487,8 @@
     Gift[] arrGiftConv = null;
     if (arrGiftCopy != null) {
       arrGiftConv = new Gift[arrGiftCopy.length];
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       for (int i = 0; i < arrGiftCopy.length; i++) {
-        arrGiftConv[i] = factory.fromGenericDocument(arrGiftCopy[i]);
+        arrGiftConv[i] = arrGiftCopy[i].toDocumentClass(Gift.class);
       }
     }
     String[] stringCopy = genericDoc.getPropertyStringArray("string");
@@ -638,45 +534,46 @@
     GenericDocument giftCopy = genericDoc.getPropertyDocument("gift");
     Gift giftConv = null;
     if (giftCopy != null) {
-      giftConv = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).fromGenericDocument(giftCopy);
+      giftConv = giftCopy.toDocumentClass(Gift.class);
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.collectLong = collectLongConv;
-    dataClass.collectInteger = collectIntegerConv;
-    dataClass.collectDouble = collectDoubleConv;
-    dataClass.collectFloat = collectFloatConv;
-    dataClass.collectBoolean = collectBooleanConv;
-    dataClass.collectByteArr = collectByteArrConv;
-    dataClass.collectString = collectStringConv;
-    dataClass.collectGift = collectGiftConv;
-    dataClass.arrBoxLong = arrBoxLongConv;
-    dataClass.arrUnboxLong = arrUnboxLongConv;
-    dataClass.arrBoxInteger = arrBoxIntegerConv;
-    dataClass.arrUnboxInt = arrUnboxIntConv;
-    dataClass.arrBoxDouble = arrBoxDoubleConv;
-    dataClass.arrUnboxDouble = arrUnboxDoubleConv;
-    dataClass.arrBoxFloat = arrBoxFloatConv;
-    dataClass.arrUnboxFloat = arrUnboxFloatConv;
-    dataClass.arrBoxBoolean = arrBoxBooleanConv;
-    dataClass.arrUnboxBoolean = arrUnboxBooleanConv;
-    dataClass.arrUnboxByteArr = arrUnboxByteArrConv;
-    dataClass.boxByteArr = boxByteArrConv;
-    dataClass.arrString = arrStringConv;
-    dataClass.arrGift = arrGiftConv;
-    dataClass.string = stringConv;
-    dataClass.boxLong = boxLongConv;
-    dataClass.unboxLong = unboxLongConv;
-    dataClass.boxInteger = boxIntegerConv;
-    dataClass.unboxInt = unboxIntConv;
-    dataClass.boxDouble = boxDoubleConv;
-    dataClass.unboxDouble = unboxDoubleConv;
-    dataClass.boxFloat = boxFloatConv;
-    dataClass.unboxFloat = unboxFloatConv;
-    dataClass.boxBoolean = boxBooleanConv;
-    dataClass.unboxBoolean = unboxBooleanConv;
-    dataClass.unboxByteArr = unboxByteArrConv;
-    dataClass.gift = giftConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.collectLong = collectLongConv;
+    document.collectInteger = collectIntegerConv;
+    document.collectDouble = collectDoubleConv;
+    document.collectFloat = collectFloatConv;
+    document.collectBoolean = collectBooleanConv;
+    document.collectByteArr = collectByteArrConv;
+    document.collectString = collectStringConv;
+    document.collectGift = collectGiftConv;
+    document.arrBoxLong = arrBoxLongConv;
+    document.arrUnboxLong = arrUnboxLongConv;
+    document.arrBoxInteger = arrBoxIntegerConv;
+    document.arrUnboxInt = arrUnboxIntConv;
+    document.arrBoxDouble = arrBoxDoubleConv;
+    document.arrUnboxDouble = arrUnboxDoubleConv;
+    document.arrBoxFloat = arrBoxFloatConv;
+    document.arrUnboxFloat = arrUnboxFloatConv;
+    document.arrBoxBoolean = arrBoxBooleanConv;
+    document.arrUnboxBoolean = arrUnboxBooleanConv;
+    document.arrUnboxByteArr = arrUnboxByteArrConv;
+    document.boxByteArr = boxByteArrConv;
+    document.arrString = arrStringConv;
+    document.arrGift = arrGiftConv;
+    document.string = stringConv;
+    document.boxLong = boxLongConv;
+    document.unboxLong = unboxLongConv;
+    document.boxInteger = boxIntegerConv;
+    document.unboxInt = unboxIntConv;
+    document.boxDouble = boxDoubleConv;
+    document.unboxDouble = unboxDoubleConv;
+    document.boxFloat = boxFloatConv;
+    document.unboxFloat = unboxFloatConv;
+    document.boxBoolean = boxBooleanConv;
+    document.unboxBoolean = unboxBooleanConv;
+    document.unboxByteArr = unboxByteArrConv;
+    document.gift = giftConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
index 9be6cdb..d938e9e 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
@@ -1,47 +1,47 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("tokNone")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNone")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("tokPlain")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlain")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String tokNoneCopy = dataClass.tokNone;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String tokNoneCopy = document.tokNone;
     if (tokNoneCopy != null) {
       builder.setPropertyString("tokNone", tokNoneCopy);
     }
-    String tokPlainCopy = dataClass.tokPlain;
+    String tokPlainCopy = document.tokPlain;
     if (tokPlainCopy != null) {
       builder.setPropertyString("tokPlain", tokPlainCopy);
     }
@@ -50,7 +50,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] tokNoneCopy = genericDoc.getPropertyStringArray("tokNone");
     String tokNoneConv = null;
     if (tokNoneCopy != null && tokNoneCopy.length != 0) {
@@ -61,10 +62,11 @@
     if (tokPlainCopy != null && tokPlainCopy.length != 0) {
       tokPlainConv = tokPlainCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.tokNone = tokNoneConv;
-    dataClass.tokPlain = tokPlainConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.tokNone = tokNoneConv;
+    document.tokPlain = tokPlainConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
new file mode 100644
index 0000000..6ad286c
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleConventions.JAVA
@@ -0,0 +1,64 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Override;
+import java.lang.String;
+import javax.annotation.Generated;
+
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
+
+  @Override
+  public String getSchemaName() {
+    return SCHEMA_NAME;
+  }
+
+  @Override
+  public AppSearchSchema getSchema() throws AppSearchException {
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price1")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .build())
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price2")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .build())
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price3")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .build())
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price4")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .build())
+          .build();
+  }
+
+  @Override
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+    GenericDocument.Builder<?> builder =
+        new GenericDocument.Builder<>(document.namespace, document.getId(), SCHEMA_NAME);
+    builder.setPropertyLong("price1", document.getPrice1());
+    builder.setPropertyLong("price2", document.price2());
+    builder.setPropertyLong("price3", document.getPrice3());
+    builder.setPropertyLong("price4", document.price4_);
+    return builder.build();
+  }
+
+  @Override
+  public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
+    String id_Conv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
+    int price1Conv = (int) genericDoc.getPropertyLong("price1");
+    int mPrice2Conv = (int) genericDoc.getPropertyLong("price2");
+    int _price3Conv = (int) genericDoc.getPropertyLong("price3");
+    int price4_Conv = (int) genericDoc.getPropertyLong("price4");
+    Gift document = new Gift(id_Conv, price4_Conv);
+    document.namespace = namespaceConv;
+    document.setPrice1(price1Conv);
+    document.price2(mPrice2Conv);
+    document.price3(_price3Conv);
+    return document;
+  }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
index 890d43d..9c71ee4 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
@@ -1,47 +1,48 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    builder.setPropertyLong("price", dataClass.getPrice());
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.getPrice());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.setPrice(priceConv);
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.setPrice(priceConv);
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
new file mode 100644
index 0000000..e3a8df1
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_usableFactoryMethod_unusableConstructor.JAVA
@@ -0,0 +1,47 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Override;
+import java.lang.String;
+import javax.annotation.Generated;
+
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
+
+  @Override
+  public String getSchemaName() {
+    return SCHEMA_NAME;
+  }
+
+  @Override
+  public AppSearchSchema getSchema() throws AppSearchException {
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("price")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .build())
+          .build();
+  }
+
+  @Override
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+    GenericDocument.Builder<?> builder =
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.price);
+    return builder.build();
+  }
+
+  @Override
+  public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
+    int priceConv = (int) genericDoc.getPropertyLong("price");
+    Gift document = Gift.create(idConv, namespaceConv, priceConv);
+    document.namespace = namespaceConv;
+    document.price = priceConv;
+    return document;
+  }
+}
diff --git a/work/workmanager-gcm/api/2.6.0-beta01.txt b/appsearch/debug-view/api/current.txt
similarity index 100%
rename from work/workmanager-gcm/api/2.6.0-beta01.txt
rename to appsearch/debug-view/api/current.txt
diff --git a/work/workmanager-gcm/api/2.6.0-beta01.txt b/appsearch/debug-view/api/public_plus_experimental_current.txt
similarity index 100%
copy from work/workmanager-gcm/api/2.6.0-beta01.txt
copy to appsearch/debug-view/api/public_plus_experimental_current.txt
diff --git a/work/workmanager-gcm/api/res-2.6.0-beta01.txt b/appsearch/debug-view/api/res-current.txt
similarity index 100%
rename from work/workmanager-gcm/api/res-2.6.0-beta01.txt
rename to appsearch/debug-view/api/res-current.txt
diff --git a/work/workmanager-gcm/api/2.6.0-beta01.txt b/appsearch/debug-view/api/restricted_current.txt
similarity index 100%
copy from work/workmanager-gcm/api/2.6.0-beta01.txt
copy to appsearch/debug-view/api/restricted_current.txt
diff --git a/appsearch/debug-view/build.gradle b/appsearch/debug-view/build.gradle
new file mode 100644
index 0000000..6621325
--- /dev/null
+++ b/appsearch/debug-view/build.gradle
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+android {
+    defaultConfig {
+        multiDexEnabled true
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation project(':appsearch:appsearch')
+    implementation project(':appsearch:appsearch-local-storage')
+    implementation project(':appsearch:appsearch-platform-storage')
+    implementation('androidx.concurrent:concurrent-futures:1.0.0')
+    implementation('androidx.core:core:1.5.0')
+    implementation('androidx.fragment:fragment:1.3.0')
+    implementation('androidx.legacy:legacy-support-v4:1.0.0')
+    implementation("androidx.lifecycle:lifecycle-livedata:2.0.0")
+    implementation('androidx.multidex:multidex:2.0.1')
+    implementation('com.google.android.material:material:1.0.0')
+    implementation(libs.constraintLayout)
+    implementation(libs.guavaAndroid)
+}
+
+androidx {
+    name = "AndroidX AppSearch Debug View"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.APPSEARCH
+    mavenVersion = LibraryVersions.APPSEARCH
+    inceptionYear = "2021"
+    description = "A support library for AndroidX AppSearch that contains activities and views " +
+            "for debugging an application's integration with AppSearch."
+}
diff --git a/appsearch/debug-view/samples/build.gradle b/appsearch/debug-view/samples/build.gradle
new file mode 100644
index 0000000..3e1c5ac
--- /dev/null
+++ b/appsearch/debug-view/samples/build.gradle
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.appsearch.debugview.sample"
+        versionCode 1
+        versionName "1.0"
+        multiDexEnabled true
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    annotationProcessor project(":appsearch:appsearch-compiler")
+
+    api('androidx.annotation:annotation:1.1.0')
+
+    implementation project(':appsearch:appsearch')
+    implementation project(':appsearch:appsearch-local-storage')
+    implementation project(':appsearch:appsearch-debug-view')
+    implementation('androidx.appcompat:appcompat:1.2.0')
+    implementation('androidx.concurrent:concurrent-futures:1.0.0')
+    implementation('androidx.multidex:multidex:2.0.1')
+    implementation('com.google.android.material:material:1.0.0')
+    implementation('com.google.code.gson:gson:2.6.2')
+    implementation(libs.constraintLayout)
+    implementation(libs.guavaAndroid)
+}
+
+androidx {
+    name = "AndroidX AppSearch Debug View Sample App"
+    type = LibraryType.SAMPLES
+    mavenGroup = LibraryGroups.APPSEARCH
+    mavenVersion = LibraryVersions.APPSEARCH
+    inceptionYear = "2021"
+    description = "Contains a sample app for integrating the Androidx AppSearch Debug View"
+}
diff --git a/appsearch/debug-view/samples/src/main/AndroidManifest.xml b/appsearch/debug-view/samples/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a295ffb3
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.appsearch.debugview.samples">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat">
+        <activity android:name=".NotesActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity android:name="androidx.appsearch.debugview.view.AppSearchDebugActivity" />
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/assets/sample_notes.json b/appsearch/debug-view/samples/src/main/assets/sample_notes.json
new file mode 100644
index 0000000..18438c0
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/assets/sample_notes.json
@@ -0,0 +1,34 @@
+{
+  "data" : [
+    {
+      "noteText": "Don't forget to grab lunch!",
+      "namespace": "namespace1",
+      "id": "note1"
+    },
+    {
+      "noteText": "I wonder what food I should get.",
+      "namespace": "namespace1",
+      "id": "note2"
+    },
+    {
+      "noteText": "Apples are my favorite fruit.",
+      "namespace": "namespace1",
+      "id": "note3"
+    },
+    {
+      "noteText": "The weather is great!",
+      "namespace": "namespace2",
+      "id": "note1"
+    },
+    {
+      "noteText": "I hope it doesn't rain.",
+      "namespace": "namespace2",
+      "id": "note2"
+    },
+    {
+      "noteText": "Tomorrow will be hot.",
+      "namespace": "namespace2",
+      "id": "note3"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
new file mode 100644
index 0000000..bddb5fb
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.samples;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appsearch.debugview.samples.model.Note;
+import androidx.appsearch.debugview.view.AppSearchDebugActivity;
+import androidx.core.content.ContextCompat;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/**
+ * Default Activity for AppSearch Debug View Sample App
+ *
+ * <p>This activity reads sample data, converts it into {@link Note} objects, and then indexes
+ * them into AppSearch.
+ *
+ * <p>Each sample note's text is added to the list view for display.
+ */
+public class NotesActivity extends AppCompatActivity {
+    private static final String DB_NAME = "notesDb";
+    private static final String SAMPLE_NOTES_FILENAME = "sample_notes.json";
+    private static final String TAG = "NotesActivity";
+
+    private final SettableFuture<NotesAppSearchManager> mNotesAppSearchManagerFuture =
+            SettableFuture.create();
+    private ArrayAdapter<Note> mNotesAdapter;
+    private ListView mListView;
+    private TextView mLoadingView;
+    private ListeningExecutorService mBackgroundExecutor;
+    private List<Note> mSampleNotes;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_notes);
+
+        mListView = findViewById(R.id.list_view);
+        mLoadingView = findViewById(R.id.text_view);
+
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+
+        mNotesAppSearchManagerFuture.setFuture(NotesAppSearchManager.createNotesAppSearchManager(
+                getApplicationContext(), mBackgroundExecutor));
+        ListenableFuture<List<Note>> sampleNotesFuture =
+                mBackgroundExecutor.submit(() -> loadSampleNotes());
+
+        ListenableFuture<Void> insertNotesFuture =
+                Futures.whenAllSucceed(mNotesAppSearchManagerFuture, sampleNotesFuture).call(
+                        () -> {
+                            mSampleNotes = Futures.getDone(sampleNotesFuture);
+                            Futures.getDone(mNotesAppSearchManagerFuture).insertNotes(
+                                    mSampleNotes).get();
+                            return null;
+                        }, mBackgroundExecutor);
+
+        Futures.addCallback(insertNotesFuture,
+                new FutureCallback<Void>() {
+                    @Override
+                    public void onSuccess(Void result) {
+                        displayNotes();
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Toast.makeText(NotesActivity.this, "Failed to insert notes "
+                                + "into AppSearch.", Toast.LENGTH_LONG).show();
+                        Log.e(TAG, "Failed to insert notes into AppSearch.", t);
+                    }
+                }, ContextCompat.getMainExecutor(this));
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+        getMenuInflater().inflate(R.menu.debug_menu, menu);
+
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+        switch (item.getItemId()) {
+            case (R.id.app_search_debug):
+                Intent intent = new Intent(this, AppSearchDebugActivity.class);
+                intent.putExtra(AppSearchDebugActivity.DB_INTENT_KEY, DB_NAME);
+                intent.putExtra(AppSearchDebugActivity.STORAGE_TYPE_INTENT_KEY,
+                        AppSearchDebugActivity.STORAGE_TYPE_LOCAL);
+                startActivity(intent);
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void onStop() {
+        Futures.whenAllSucceed(mNotesAppSearchManagerFuture).call(() -> {
+            Futures.getDone(mNotesAppSearchManagerFuture).close();
+            return null;
+        }, mBackgroundExecutor);
+
+        super.onStop();
+    }
+
+    @WorkerThread
+    private List<Note> loadSampleNotes() {
+        List<Note> sampleNotes = new ArrayList<>();
+        Gson gson = new Gson();
+        try (InputStreamReader r = new InputStreamReader(
+                getAssets().open(SAMPLE_NOTES_FILENAME))) {
+            JsonObject samplesJson = gson.fromJson(r, JsonObject.class);
+            JsonArray sampleJsonArr = samplesJson.getAsJsonArray("data");
+            for (int i = 0; i < sampleJsonArr.size(); ++i) {
+                JsonObject noteJson = sampleJsonArr.get(i).getAsJsonObject();
+                sampleNotes.add(new Note.Builder().setId(noteJson.get("id").getAsString())
+                        .setNamespace(noteJson.get("namespace").getAsString())
+                        .setText(noteJson.get("noteText").getAsString())
+                        .build()
+                );
+            }
+        } catch (IOException e) {
+            Toast.makeText(NotesActivity.this, "Failed to load sample notes ",
+                    Toast.LENGTH_LONG).show();
+            Log.e(TAG, "Sample notes IO failed: ", e);
+        }
+        return sampleNotes;
+    }
+
+    private void displayNotes() {
+        mNotesAdapter = new ArrayAdapter<>(this,
+                android.R.layout.simple_list_item_1, mSampleNotes);
+        mListView.setAdapter(mNotesAdapter);
+
+        mLoadingView.setVisibility(View.GONE);
+        mListView.setVisibility(View.VISIBLE);
+    }
+}
diff --git a/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
new file mode 100644
index 0000000..7aba8a7
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.samples;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.debugview.samples.model.Note;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.LocalStorage;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.Closeable;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Manages interactions with AppSearch.
+ */
+public class NotesAppSearchManager implements Closeable {
+    private static final String DB_NAME = "notesDb";
+    private static final boolean FORCE_OVERRIDE = true;
+
+    private final Context mContext;
+    private final Executor mExecutor;
+    private final SettableFuture<AppSearchSession> mAppSearchSessionFuture =
+            SettableFuture.create();
+
+    private NotesAppSearchManager(@NonNull Context context, @NonNull Executor executor) {
+        mContext = context;
+        mExecutor = executor;
+    }
+
+    /**
+     * Factory for creating a {@link NotesAppSearchManager} instance.
+     *
+     * <p>This creates and initializes an {@link AppSearchSession}. It also resets existing
+     * {@link Note} objects from the index and re-adds the {@link Note} document class to the
+     * AppSearch schema.
+     *
+     * @param executor to run AppSearch operations on.
+     */
+    @NonNull
+    public static ListenableFuture<NotesAppSearchManager> createNotesAppSearchManager(
+            @NonNull Context context, @NonNull Executor executor) {
+        NotesAppSearchManager notesAppSearchManager = new NotesAppSearchManager(context, executor);
+        return Futures.transform(notesAppSearchManager.initialize(),
+                unused -> notesAppSearchManager, executor);
+    }
+
+    /**
+     * Closes the AppSearch session.
+     */
+    @Override
+    public void close() {
+        Futures.whenAllSucceed(mAppSearchSessionFuture).call(() -> {
+            Futures.getDone(mAppSearchSessionFuture).close();
+            return null;
+        }, mExecutor);
+    }
+
+    /**
+     * Inserts {@link Note} documents into the AppSearch database.
+     *
+     * @param notes list of notes to index in AppSearch.
+     */
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, Void>> insertNotes(
+            @NonNull List<Note> notes) {
+        try {
+            PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(notes)
+                    .build();
+            return Futures.transformAsync(mAppSearchSessionFuture,
+                    session -> session.put(request), mExecutor);
+        } catch (Exception e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+
+    @NonNull
+    private ListenableFuture<Void> initialize() {
+        return Futures.transformAsync(createLocalSession(), session -> {
+            mAppSearchSessionFuture.set(session);
+            return Futures.transformAsync(resetDocuments(),
+                    unusedResetResult -> Futures.transform(setSchema(),
+                            unusedSetSchemaResult -> null,
+                            mExecutor),
+                    mExecutor);
+        }, mExecutor);
+    }
+
+    private ListenableFuture<AppSearchSession> createLocalSession() {
+        return LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(mContext, DB_NAME)
+                        .build()
+        );
+    }
+
+    private ListenableFuture<SetSchemaResponse> resetDocuments() {
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().setForceOverride(FORCE_OVERRIDE).build();
+        return Futures.transformAsync(mAppSearchSessionFuture,
+                session -> session.setSchema(request),
+                mExecutor);
+    }
+
+    private ListenableFuture<SetSchemaResponse> setSchema() {
+        try {
+            SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Note.class)
+                    .build();
+            return Futures.transformAsync(mAppSearchSessionFuture,
+                    session -> session.setSchema(request), mExecutor);
+        } catch (AppSearchException e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+}
diff --git a/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/model/Note.java b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/model/Note.java
new file mode 100644
index 0000000..07cf0a2
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/model/Note.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.samples.model;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.core.util.Preconditions;
+
+/**
+ * Encapsulates a Note document.
+ */
+@Document
+public class Note {
+
+    Note(@NonNull String namespace, @NonNull String id, @NonNull String text) {
+        mId = Preconditions.checkNotNull(id);
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mText = Preconditions.checkNotNull(text);
+    }
+
+    @Document.Id
+    private final String mId;
+
+    @Document.Namespace
+    private final String mNamespace;
+
+    @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+    private final String mText;
+
+    /** Returns the ID of the {@link Note} object. */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    /** Returns the namespace of the {@link Note} object. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the text of the {@link Note} object. */
+    @NonNull
+    public String getText() {
+        return mText;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return mText;
+    }
+
+    /** Builder for {@link Note} objects. */
+    public static final class Builder {
+        private String mNamespace = "";
+        private String mId = "";
+        private String mText = "";
+
+        /** Sets the namespace of the {@link Note} object. */
+        @NonNull
+        public Note.Builder setNamespace(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+            return this;
+        }
+
+        /** Sets the ID of the {@link Note} object. */
+        @NonNull
+        public Note.Builder setId(@NonNull String id) {
+            mId = Preconditions.checkNotNull(id);
+            return this;
+        }
+
+        /** Sets the text of the {@link Note} object. */
+        @NonNull
+        public Note.Builder setText(@NonNull String text) {
+            mText = Preconditions.checkNotNull(text);
+            return this;
+        }
+
+        /** Creates a new {@link Note} object. */
+        @NonNull
+        public Note build() {
+            return new Note(mNamespace, mId, mText);
+        }
+    }
+}
diff --git a/appsearch/debug-view/samples/src/main/res/drawable-v24/ic_launcher_foreground.xml b/appsearch/debug-view/samples/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..cb0581c
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,46 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeColor="#00000000"
+        android:strokeWidth="1" />
+</vector>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/drawable/ic_launcher_background.xml b/appsearch/debug-view/samples/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..6dcf0d3
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+</vector>
diff --git a/appsearch/debug-view/samples/src/main/res/layout/activity_notes.xml b/appsearch/debug-view/samples/src/main/res/layout/activity_notes.xml
new file mode 100644
index 0000000..6ce0f0f4
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/layout/activity_notes.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".NotesActivity">
+
+    <TextView
+        android:id="@+id/text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="Loading sample notes..."
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <ListView
+        android:id="@+id/list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.appcompat.widget.LinearLayoutCompat>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/menu/debug_menu.xml b/appsearch/debug-view/samples/src/main/res/menu/debug_menu.xml
new file mode 100644
index 0000000..6ba449f
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/menu/debug_menu.xml
@@ -0,0 +1,7 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/app_search_debug"
+        android:title="AppSearch Debug View"
+        >
+    </item>
+</menu>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..8da4add9
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..8da4add9
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/values/colors.xml b/appsearch/debug-view/samples/src/main/res/values/colors.xml
new file mode 100644
index 0000000..cde477b
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+</resources>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/values/strings.xml b/appsearch/debug-view/samples/src/main/res/values/strings.xml
new file mode 100644
index 0000000..41c6b20
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <string name="app_name">AppSearch Debug View Sample App</string>
+</resources>
\ No newline at end of file
diff --git a/appsearch/debug-view/src/main/AndroidManifest.xml b/appsearch/debug-view/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..043ccbf
--- /dev/null
+++ b/appsearch/debug-view/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+<manifest package="androidx.appsearch.debugview" />
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
new file mode 100644
index 0000000..11b6755
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.debugview.view.AppSearchDebugActivity;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.LocalStorage;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Manages interactions with AppSearch.
+ *
+ * <p>Instances of {@link DebugAppSearchManager} are created by calling {@link #create}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DebugAppSearchManager implements Closeable {
+    private static final int PAGE_SIZE = 100;
+
+    private final Context mContext;
+    private final ExecutorService mExecutor;
+    private final SettableFuture<AppSearchSession> mAppSearchSessionFuture =
+            SettableFuture.create();
+
+    private DebugAppSearchManager(@NonNull Context context, @NonNull ExecutorService executor) {
+        mContext = Preconditions.checkNotNull(context);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    /**
+     * Factory for creating a {@link DebugAppSearchManager} instance.
+     *
+     * <p>This factory creates an {@link AppSearchSession} instance with the provided
+     * database name.
+     *
+     * @param context      application context.
+     * @param executor     executor to run AppSearch operations on.
+     * @param databaseName name of the database to open AppSearch debugging for.
+     * @param storageType  constant of the storage type to start session for.
+     * @throws AppSearchException if the storage type is invalid, or a R- device selects platform
+     *                            storage as the storage type for debugging.
+     */
+    @NonNull
+    public static ListenableFuture<DebugAppSearchManager> create(
+            @NonNull Context context,
+            @NonNull ExecutorService executor, @NonNull String databaseName,
+            @AppSearchDebugActivity.StorageType int storageType) throws AppSearchException {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(executor);
+        Preconditions.checkNotNull(databaseName);
+
+        DebugAppSearchManager debugAppSearchManager =
+                new DebugAppSearchManager(context, executor);
+
+        ListenableFuture<DebugAppSearchManager> debugAppSearchManagerListenableFuture;
+
+        switch (storageType) {
+            case AppSearchDebugActivity.STORAGE_TYPE_LOCAL:
+                debugAppSearchManagerListenableFuture =
+                        Futures.transform(
+                                debugAppSearchManager.initializeLocalStorage(databaseName),
+                                unused -> debugAppSearchManager, executor);
+                break;
+            case AppSearchDebugActivity.STORAGE_TYPE_PLATFORM:
+                if (BuildCompat.isAtLeastS()) {
+                    debugAppSearchManagerListenableFuture =
+                            Futures.transform(
+                                    debugAppSearchManager.initializePlatformStorage(databaseName),
+                                    unused -> debugAppSearchManager, executor);
+                } else {
+                    throw new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT,
+                            "Platform Storage debugging only valid for S+ devices.");
+                }
+                break;
+            default:
+                throw new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT,
+                        "Invalid storage type specified. Verify that the "
+                                + "storage type that has been passed in the intent is valid.");
+        }
+        return debugAppSearchManagerListenableFuture;
+    }
+
+    /**
+     * Searches for all documents in the AppSearch database.
+     *
+     * <p>Each {@link GenericDocument} object is truncated of its properties by adding
+     * projection to the request.
+     *
+     * @return the {@link SearchResults} instance for exploring pages of results. Call
+     * {@link #getNextPage} to retrieve the {@link GenericDocument} objects for each page.
+     */
+    @NonNull
+    public ListenableFuture<SearchResults> getAllDocumentsSearchResults() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setResultCountPerPage(PAGE_SIZE)
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
+                .build();
+        String retrieveAllQueryString = "";
+
+        return Futures.transform(mAppSearchSessionFuture,
+                session -> session.search(retrieveAllQueryString, searchSpec), mExecutor);
+    }
+
+
+    /**
+     * Converts the next page from the provided {@link SearchResults} instance to a list of
+     * {@link GenericDocument} objects.
+     *
+     * @param results results to get next page for, and convert to a list of
+     *                {@link GenericDocument} objects.
+     */
+    @NonNull
+    public ListenableFuture<List<GenericDocument>> getNextPage(@NonNull SearchResults results) {
+        Preconditions.checkNotNull(results);
+
+        return Futures.transform(results.getNextPage(),
+                DebugAppSearchManager::convertResultsToGenericDocuments, mExecutor);
+    }
+
+    /**
+     * Gets a document from the AppSearch database by namespace and ID.
+     */
+    @NonNull
+    public ListenableFuture<GenericDocument> getDocument(@NonNull String namespace,
+            @NonNull String id) {
+        Preconditions.checkNotNull(id);
+        Preconditions.checkNotNull(namespace);
+        GetByDocumentIdRequest request =
+                new GetByDocumentIdRequest.Builder(namespace).addIds(id).build();
+
+        return Futures.transformAsync(mAppSearchSessionFuture,
+                session -> Futures.transform(session.getByDocumentId(request),
+                        response -> response.getSuccesses().get(id), mExecutor), mExecutor);
+    }
+
+    /**
+     * Gets the schema of the AppSearch database.
+     */
+    @NonNull
+    public ListenableFuture<GetSchemaResponse> getSchema() {
+        return Futures.transformAsync(mAppSearchSessionFuture,
+                session -> session.getSchema(), mExecutor);
+    }
+
+    /**
+     * Closes the AppSearch session.
+     */
+    @Override
+    public void close() {
+        Futures.whenAllSucceed(mAppSearchSessionFuture).call(() -> {
+            Futures.getDone(mAppSearchSessionFuture).close();
+            return null;
+        }, mExecutor);
+    }
+
+    @NonNull
+    private ListenableFuture<AppSearchSession> initializeLocalStorage(
+            @NonNull String databaseName) {
+        mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(mContext, databaseName)
+                        .build())
+        );
+        return mAppSearchSessionFuture;
+    }
+
+    @NonNull
+    @RequiresApi(Build.VERSION_CODES.S)
+    private ListenableFuture<AppSearchSession> initializePlatformStorage(
+            @NonNull String databaseName) {
+        mAppSearchSessionFuture.setFuture(PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(mContext, databaseName)
+                        .build())
+        );
+        return mAppSearchSessionFuture;
+    }
+
+    private static List<GenericDocument> convertResultsToGenericDocuments(
+            List<SearchResult> results) {
+        List<GenericDocument> docs = new ArrayList<>(results.size());
+
+        for (SearchResult result : results) {
+            docs.add(result.getGenericDocument());
+        }
+
+        return docs;
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
new file mode 100644
index 0000000..5a23cb0
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.model;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.core.util.Preconditions;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Documents ViewModel for the database's {@link GenericDocument} objects.
+ *
+ * <p>This model captures the data for displaying lists of {@link GenericDocument} objects. Each
+ * {@link GenericDocument} object is truncated of all properties.
+ *
+ * <p>Instances of {@link DocumentListModel} are created by {@link DocumentListModelFactory}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentListModel extends ViewModel {
+    private static final String TAG = "DocumentListModel";
+
+    private final ExecutorService mExecutor;
+    private final DebugAppSearchManager mDebugAppSearchManager;
+    final MutableLiveData<List<GenericDocument>> mDocumentsLiveData =
+            new MutableLiveData<>();
+    final MutableLiveData<SearchResults> mDocumentsSearchResultsLiveData =
+            new MutableLiveData<>();
+    volatile boolean mHasAdditionalPages = true;
+
+    public DocumentListModel(@NonNull ExecutorService executor,
+            @NonNull DebugAppSearchManager debugAppSearchManager) {
+        mExecutor = Preconditions.checkNotNull(executor);
+        mDebugAppSearchManager = Preconditions.checkNotNull(debugAppSearchManager);
+    }
+
+    /**
+     * Gets the {@link SearchResults} instance for a search over all documents in the AppSearch
+     * database.
+     *
+     * <p>Call {@link #addAdditionalResultsPage} to get the next page of documents from the
+     * {@link SearchResults} instance.
+     *
+     * <p>This should only be called once per fragment.
+     */
+    @NonNull
+    public LiveData<SearchResults> getAllDocumentsSearchResults() {
+        Futures.addCallback(mDebugAppSearchManager.getAllDocumentsSearchResults(),
+                new FutureCallback<SearchResults>() {
+                    @Override
+                    public void onSuccess(SearchResults result) {
+                        // There should only be one active observer to post this value to as its
+                        // called only once per fragment, ensuring a safe null check.
+                        if (mDocumentsSearchResultsLiveData.getValue() == null) {
+                            mDocumentsSearchResultsLiveData.postValue(result);
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Log.e(TAG, "Failed to get all documents.", t);
+                    }
+                }, mExecutor);
+        return mDocumentsSearchResultsLiveData;
+    }
+
+    /**
+     * Adds the next page of documents for the provided {@link SearchResults} instance to the
+     * running list of retrieved {@link GenericDocument} objects.
+     *
+     * <p>Each page is represented as a list of {@link GenericDocument} objects.
+     *
+     * @return a {@link LiveData} encapsulating the list of {@link GenericDocument} objects for
+     * documents retrieved from all previous pages and this additional page.
+     */
+    @NonNull
+    public LiveData<List<GenericDocument>> addAdditionalResultsPage(
+            @NonNull SearchResults results) {
+        Futures.addCallback(mDebugAppSearchManager.getNextPage(results),
+                new FutureCallback<List<GenericDocument>>() {
+                    @Override
+                    public void onSuccess(List<GenericDocument> result) {
+                        if (mDocumentsLiveData.getValue() == null) {
+                            mDocumentsLiveData.postValue(result);
+                        } else {
+                            if (result.isEmpty()) {
+                                mHasAdditionalPages = false;
+                            }
+                            mDocumentsLiveData.getValue().addAll(result);
+                            mDocumentsLiveData.postValue(mDocumentsLiveData.getValue());
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Log.e(TAG, "Failed to get next page of documents.", t);
+                    }
+                }, mExecutor);
+
+        return mDocumentsLiveData;
+    }
+
+    /**
+     * Returns whether there are additional pages to load to the document list.
+     */
+    public boolean hasAdditionalPages() {
+        return mHasAdditionalPages;
+    }
+
+    /**
+     * Gets all {@link GenericDocument} objects that have been loaded.
+     *
+     * <p>If the underlying list of the Documents LiveData is {@code null}, this returns an
+     * empty list as a placeholder.
+     */
+    @NonNull
+    public List<GenericDocument> getAllLoadedDocuments() {
+        if (mDocumentsLiveData.getValue() == null) {
+            return Collections.emptyList();
+        }
+        return mDocumentsLiveData.getValue();
+    }
+
+    /**
+     * Factory for creating a {@link DocumentListModel} instance.
+     */
+    public static class DocumentListModelFactory extends ViewModelProvider.NewInstanceFactory {
+        private final DebugAppSearchManager mDebugAppSearchManager;
+        private final ListeningExecutorService mExecutorService;
+
+        public DocumentListModelFactory(@NonNull ListeningExecutorService executor,
+                @NonNull DebugAppSearchManager debugAppSearchManager) {
+            mDebugAppSearchManager = debugAppSearchManager;
+            mExecutorService = executor;
+        }
+
+        @SuppressWarnings("unchecked")
+        @NonNull
+        @Override
+        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+            if (modelClass == DocumentListModel.class) {
+                return (T) new DocumentListModel(mExecutorService, mDebugAppSearchManager);
+            } else {
+                throw new IllegalArgumentException("Expected class: DocumentListModel.");
+            }
+        }
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
new file mode 100644
index 0000000..a8c03dc
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.model;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.core.util.Preconditions;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Document ViewModel for displaying a {@link GenericDocument} object.
+ *
+ * <p>Instances of the ViewModel are created by {@link DocumentModelFactory}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentModel extends ViewModel {
+    private static final String TAG = "AppSearchDocumentModel";
+
+    private final ExecutorService mExecutor;
+    private final DebugAppSearchManager mDebugAppSearchManager;
+    final MutableLiveData<GenericDocument> mDocumentLiveData = new MutableLiveData<>();
+
+    public DocumentModel(@NonNull ExecutorService executor,
+            @NonNull DebugAppSearchManager debugAppSearchManager) {
+        mExecutor = Preconditions.checkNotNull(executor);
+        mDebugAppSearchManager = Preconditions.checkNotNull(debugAppSearchManager);
+    }
+
+    /**
+     * Gets a {@link GenericDocument} object by namespace and ID.
+     */
+    @NonNull
+    public LiveData<GenericDocument> getDocument(@NonNull String namespace, @NonNull String id) {
+        Futures.addCallback(mDebugAppSearchManager.getDocument(namespace, id),
+                new FutureCallback<GenericDocument>() {
+                    @Override
+                    public void onSuccess(GenericDocument result) {
+                        mDocumentLiveData.postValue(result);
+                    }
+
+                    @Override
+                    public void onFailure(@Nullable Throwable t) {
+                        Log.e(TAG,
+                                "Failed to get document with namespace: " + namespace + " and "
+                                        + "id: " + id, t);
+                    }
+                }, mExecutor);
+        return mDocumentLiveData;
+    }
+
+    /**
+     * Factory for creating a {@link DocumentModel} instance.
+     */
+    public static class DocumentModelFactory extends ViewModelProvider.NewInstanceFactory {
+        private final DebugAppSearchManager mDebugAppSearchManager;
+        private final ListeningExecutorService mExecutorService;
+
+        public DocumentModelFactory(@NonNull ListeningExecutorService executor,
+                @NonNull DebugAppSearchManager debugAppSearchManager) {
+            mDebugAppSearchManager = debugAppSearchManager;
+            mExecutorService = executor;
+        }
+
+        @SuppressWarnings("unchecked")
+        @NonNull
+        @Override
+        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+            if (modelClass == DocumentModel.class) {
+                return (T) new DocumentModel(mExecutorService, mDebugAppSearchManager);
+            } else {
+                throw new IllegalArgumentException("Expected class: DocumentModel.");
+            }
+        }
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
new file mode 100644
index 0000000..16e0b66
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.model;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.core.util.Preconditions;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Schema Type List ViewModel for the database schema's.
+ *
+ * <p>This model captures the data for displaying a list of {@link AppSearchSchema} objects that
+ * compose of the schema. This also captures the overall schema version.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SchemaTypeListModel extends ViewModel {
+    private static final String TAG = "AppSearchSchemaTypeList";
+
+    private final ExecutorService mExecutor;
+    private final DebugAppSearchManager mDebugAppSearchManager;
+    final MutableLiveData<GetSchemaResponse> mSchemaResponseMutableLiveData =
+            new MutableLiveData<>();
+
+    public SchemaTypeListModel(@NonNull ExecutorService executor,
+            @NonNull DebugAppSearchManager debugAppSearchManager) {
+        mExecutor = Preconditions.checkNotNull(executor);
+        mDebugAppSearchManager = Preconditions.checkNotNull(debugAppSearchManager);
+    }
+
+    /**
+     * Gets list of {@link AppSearchSchema} objects that compose of the schema.
+     *
+     * @return live data of list of {@link AppSearchSchema} objects.
+     */
+    @NonNull
+    public LiveData<List<AppSearchSchema>> getSchemaTypes() {
+        return Transformations.map(getSchema(),
+                input -> new ArrayList<>(input.getSchemas()));
+    }
+
+    /**
+     * Gets overall schema version.
+     *
+     * @return live data of {@link Integer} representing the overall schema version.
+     */
+    @NonNull
+    public LiveData<Integer> getSchemaVersion() {
+        return Transformations.map(getSchema(), GetSchemaResponse::getVersion);
+    }
+
+    /**
+     * Gets schema of database.
+     *
+     * @return live data of {@link GetSchemaResponse}
+     */
+    @NonNull
+    private LiveData<GetSchemaResponse> getSchema() {
+        Futures.addCallback(mDebugAppSearchManager.getSchema(),
+                new FutureCallback<GetSchemaResponse>() {
+                    @Override
+                    public void onSuccess(GetSchemaResponse result) {
+                        mSchemaResponseMutableLiveData.postValue(result);
+                    }
+
+                    @Override
+                    public void onFailure(@Nullable Throwable t) {
+                        Log.e(TAG, "Failed to get schema.", t);
+                    }
+                }, mExecutor);
+        return mSchemaResponseMutableLiveData;
+    }
+
+    /**
+     * Factory for creating a {@link SchemaTypeListModel} instance.
+     */
+    public static class SchemaTypeListModelFactory extends ViewModelProvider.NewInstanceFactory {
+        private final DebugAppSearchManager mDebugAppSearchManager;
+        private final ListeningExecutorService mExecutorService;
+
+        public SchemaTypeListModelFactory(@NonNull ListeningExecutorService executor,
+                @NonNull DebugAppSearchManager debugAppSearchManager) {
+            mDebugAppSearchManager = debugAppSearchManager;
+            mExecutorService = executor;
+        }
+
+        @SuppressWarnings("unchecked")
+        @NonNull
+        @Override
+        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+            if (modelClass == SchemaTypeListModel.class) {
+                return (T) new SchemaTypeListModel(mExecutorService, mDebugAppSearchManager);
+            } else {
+                throw new IllegalArgumentException("Expected class: SchemaTypeListModel.");
+            }
+        }
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
new file mode 100644
index 0000000..20ea01f
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.appsearch.debugview.R;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.fragment.app.FragmentActivity;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executors;
+
+/**
+ * Debug Activity for AppSearch.
+ *
+ * <p>This activity provides a view of all the documents that have been put into an application's
+ * AppSearch database. The database is specified by creating an {@link android.content.Intent}
+ * with extras specifying the database name and the AppSearch storage type.
+ *
+ * <p>To launch this activity, declare it in the application's manifest:
+ * <pre>
+ *     <activity android:name="androidx.appsearch.debugview.view.AppSearchDebugActivity" />
+ * </pre>
+ *
+ * <p>Next, create an {@link android.content.Intent} from the activity that will launch the debug
+ * activity. Add the database name as an extra with key: {@link #DB_INTENT_KEY} and the storage
+ * type, which can be either {@link #STORAGE_TYPE_LOCAL} or {@link #STORAGE_TYPE_PLATFORM} with
+ * key: {@link #STORAGE_TYPE_INTENT_KEY}.
+ *
+ * <p>Example of launching the debug activity for local storage:
+ * <pre>
+ *     Intent intent = new Intent(this, AppSearchDebugActivity.class);
+ *     intent.putExtra(AppSearchDebugActivity.DB_INTENT_KEY, DB_NAME);
+ *     intent.putExtra(AppSearchDebugActivity.STORAGE_TYPE_INTENT_KEY,
+ *             AppSearchDebugActivity.STORAGE_TYPE_LOCAL);
+ *     startActivity(intent);
+ * </pre>
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class AppSearchDebugActivity extends FragmentActivity {
+    private static final String TAG = "AppSearchDebugActivity";
+    public static final String DB_INTENT_KEY = "databaseName";
+    public static final String STORAGE_TYPE_INTENT_KEY = "storageType";
+
+    private String mDbName;
+    private ListenableFuture<DebugAppSearchManager> mDebugAppSearchManager;
+    private ListeningExecutorService mBackgroundExecutor;
+
+    @IntDef(value = {
+            STORAGE_TYPE_LOCAL,
+            STORAGE_TYPE_PLATFORM,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StorageType {
+    }
+
+    public static final int STORAGE_TYPE_LOCAL = 0;
+    public static final int STORAGE_TYPE_PLATFORM = 1;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_appsearchdebug);
+
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        mDbName = getIntent().getExtras().getString(DB_INTENT_KEY);
+        @StorageType int storageType =
+                getIntent().getExtras().getInt(STORAGE_TYPE_INTENT_KEY);
+        try {
+            mDebugAppSearchManager = DebugAppSearchManager.create(
+                    getApplicationContext(), mBackgroundExecutor, mDbName, storageType);
+        } catch (AppSearchException e) {
+            Toast.makeText(getApplicationContext(),
+                    "Failed to initialize AppSearch: " + e.getMessage(),
+                    Toast.LENGTH_LONG).show();
+            Log.e(TAG, "Failed to initialize AppSearch.", e);
+        }
+
+        MenuFragment menuFragment = new MenuFragment();
+        getSupportFragmentManager()
+                .beginTransaction()
+                .replace(R.id.fragment_container, menuFragment)
+                .commit();
+    }
+
+    @Override
+    protected void onStop() {
+        Futures.whenAllSucceed(mDebugAppSearchManager).call(() -> {
+            Futures.getDone(mDebugAppSearchManager).close();
+            return null;
+        }, mBackgroundExecutor);
+
+        super.onStop();
+    }
+
+    /**
+     * Gets the {@link DebugAppSearchManager} instance created by the activity.
+     */
+    @NonNull
+    public ListenableFuture<DebugAppSearchManager> getDebugAppSearchManager() {
+        return mDebugAppSearchManager;
+    }
+
+    /**
+     * Gets the {@link ListeningExecutorService} instance created by the activity.
+     */
+    @NonNull
+    public ListeningExecutorService getBackgroundExecutor() {
+        return mBackgroundExecutor;
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentFragment.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentFragment.java
new file mode 100644
index 0000000..7ba316f
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentFragment.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.appsearch.debugview.R;
+import androidx.appsearch.debugview.model.DocumentModel;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+/**
+ * A fragment for displaying a {@link GenericDocument} object.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentFragment extends Fragment {
+    private static final String TAG = "AppSearchDocumentFrag";
+    private static final String ARG_NAMESPACE = "document_namespace";
+    private static final String ARG_ID = "document_id";
+
+    private String mNamespace;
+    private String mId;
+    private ListeningExecutorService mExecutor;
+    private ListenableFuture<DebugAppSearchManager> mDebugAppSearchManager;
+    private DocumentModel mDocumentModel;
+
+    /**
+     * Factory for creating a {@link DocumentFragment} instance.
+     */
+    @NonNull
+    public static DocumentFragment createDocumentFragment(
+            @NonNull String namespace, @NonNull String id) {
+        DocumentFragment documentFragment = new DocumentFragment();
+        Bundle args = new Bundle();
+        args.putString(ARG_NAMESPACE, namespace);
+        args.putString(ARG_ID, id);
+        documentFragment.setArguments(args);
+        return documentFragment;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (getArguments() != null) {
+            mId = getArguments().getString(ARG_ID);
+            mNamespace = getArguments().getString(ARG_NAMESPACE);
+        }
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_document, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mExecutor = ((AppSearchDebugActivity) getActivity()).getBackgroundExecutor();
+        mDebugAppSearchManager =
+                ((AppSearchDebugActivity) getActivity()).getDebugAppSearchManager();
+
+        Futures.addCallback(mDebugAppSearchManager,
+                new FutureCallback<DebugAppSearchManager>() {
+                    @Override
+                    public void onSuccess(DebugAppSearchManager debugAppSearchManager) {
+                        displayDocument(debugAppSearchManager);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Toast.makeText(getContext(),
+                                "Failed to initialize AppSearch: " + t.getMessage(),
+                                Toast.LENGTH_LONG).show();
+                        Log.e(TAG,
+                                "Failed to initialize AppSearch. Verify that the database name "
+                                        + "has been provided in the intent with key: databaseName",
+                                t);
+                    }
+                }, ContextCompat.getMainExecutor(getActivity()));
+    }
+
+    protected void displayDocument(@NonNull DebugAppSearchManager debugAppSearchManager) {
+        mDocumentModel =
+                new ViewModelProvider(this,
+                        new DocumentModel.DocumentModelFactory(mExecutor, debugAppSearchManager)
+                ).get(DocumentModel.class);
+
+        mDocumentModel.getDocument(mNamespace, mId).observe(this, document -> {
+            TextView documentView = getView().findViewById(R.id.document_string);
+            documentView.setText(document.toString());
+        });
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListFragment.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListFragment.java
new file mode 100644
index 0000000..6a89d55
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListFragment.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.appsearch.debugview.R;
+import androidx.appsearch.debugview.model.DocumentListModel;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.Collections;
+
+/**
+ * A fragment for displaying a list of {@link GenericDocument} objects.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentListFragment extends Fragment {
+    private static final String TAG = "DocumentListFragment";
+
+    private TextView mLoadingView;
+    private TextView mEmptyDocumentsView;
+    private RecyclerView mDocumentListRecyclerView;
+    private LinearLayoutManager mLinearLayoutManager;
+    private DocumentListItemAdapter mDocumentListItemAdapter;
+    private ListeningExecutorService mExecutor;
+    private ListenableFuture<DebugAppSearchManager> mDebugAppSearchManager;
+    private AppSearchDebugActivity mAppSearchDebugActivity;
+
+    protected boolean mLoadingPage = false;
+
+    @Nullable
+    protected DocumentListModel mDocumentListModel;
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_document_list, container, /*attachToRoot=*/
+                false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mLoadingView = getView().findViewById(R.id.loading_text_view);
+        mEmptyDocumentsView = getView().findViewById(R.id.empty_documents_text_view);
+        mDocumentListRecyclerView = getView().findViewById(R.id.document_list_recycler_view);
+
+        mAppSearchDebugActivity = (AppSearchDebugActivity) getActivity();
+        mExecutor = mAppSearchDebugActivity.getBackgroundExecutor();
+        mDebugAppSearchManager = mAppSearchDebugActivity.getDebugAppSearchManager();
+
+        initDocumentListRecyclerView();
+
+        Futures.addCallback(mDebugAppSearchManager,
+                new FutureCallback<DebugAppSearchManager>() {
+                    @Override
+                    public void onSuccess(DebugAppSearchManager debugAppSearchManager) {
+                        readDocuments(debugAppSearchManager);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Toast.makeText(getContext(),
+                                "Failed to initialize AppSearch: " + t.getMessage(),
+                                Toast.LENGTH_LONG).show();
+                        Log.e(TAG,
+                                "Failed to initialize AppSearch. Verify that the database name "
+                                        + "has been"
+                                        + " provided in the intent with key: databaseName", t);
+                    }
+                }, ContextCompat.getMainExecutor(mAppSearchDebugActivity));
+    }
+
+    /**
+     * Initializes a {@link DocumentListModel} ViewModel instance and sets observer for updating UI
+     * with document data.
+     */
+    protected void readDocuments(@NonNull DebugAppSearchManager debugAppSearchManager) {
+        mDocumentListModel =
+                new ViewModelProvider(this,
+                        new DocumentListModel.DocumentListModelFactory(mExecutor,
+                                debugAppSearchManager)).get(DocumentListModel.class);
+
+        if (mDocumentListModel.hasAdditionalPages()) {
+            mDocumentListModel.getAllDocumentsSearchResults().observe(this, results -> {
+                mLoadingView.setVisibility(View.GONE);
+                displayNextSearchResultsPage(results);
+            });
+        } else {
+            mLoadingView.setVisibility(View.GONE);
+            mDocumentListItemAdapter.setDocuments(mDocumentListModel.getAllLoadedDocuments());
+        }
+    }
+
+    private void displayNextSearchResultsPage(@NonNull SearchResults searchResults) {
+        mDocumentListModel.addAdditionalResultsPage(searchResults).observe(this, docs -> {
+            mDocumentListItemAdapter.setDocuments(docs);
+            if (docs.size() == 0) {
+                mEmptyDocumentsView.setVisibility(View.VISIBLE);
+                mDocumentListRecyclerView.setVisibility(View.GONE);
+            }
+            mLoadingPage = false;
+        });
+
+        mDocumentListRecyclerView.addOnScrollListener(
+                new ScrollListener(mLinearLayoutManager) {
+                    @Override
+                    public void loadNextPage() {
+                        mLoadingPage = true;
+                        mDocumentListModel.addAdditionalResultsPage(searchResults);
+                    }
+
+                    @Override
+                    public boolean isLoading() {
+                        return mLoadingPage;
+                    }
+
+                    @Override
+                    public boolean hasAdditionalPages() {
+                        return mDocumentListModel.hasAdditionalPages();
+                    }
+                });
+    }
+
+    private void initDocumentListRecyclerView() {
+        mLinearLayoutManager = new LinearLayoutManager(mAppSearchDebugActivity);
+        mLinearLayoutManager.setOrientation(RecyclerView.VERTICAL);
+
+        mDocumentListItemAdapter = new DocumentListItemAdapter(Collections.emptyList(), this);
+
+        mDocumentListRecyclerView.setAdapter(mDocumentListItemAdapter);
+        mDocumentListRecyclerView.setLayoutManager(mLinearLayoutManager);
+        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
+                mAppSearchDebugActivity, mLinearLayoutManager.getOrientation());
+        mDocumentListRecyclerView.addItemDecoration(dividerItemDecoration);
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListItemAdapter.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListItemAdapter.java
new file mode 100644
index 0000000..b30152b
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListItemAdapter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.debugview.R;
+import androidx.core.util.Preconditions;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * Adapter for displaying a list of {@link GenericDocument} objects.
+ *
+ * <p>This adapter displays each item as a namespace and document ID.
+ *
+ * <p>Documents can be manually changed by calling {@link #setDocuments}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentListItemAdapter extends
+        RecyclerView.Adapter<DocumentListItemAdapter.ViewHolder> {
+    private List<GenericDocument> mDocuments;
+    private DocumentListFragment mDocumentListFragment;
+
+    DocumentListItemAdapter(@NonNull List<GenericDocument> documents,
+            @NonNull DocumentListFragment documentListFragment) {
+        mDocuments = Preconditions.checkNotNull(documents);
+        mDocumentListFragment = Preconditions.checkNotNull(documentListFragment);
+    }
+
+    /**
+     * Sets the adapter's document list.
+     *
+     * @param documents list of {@link GenericDocument} objects to update adapter with.
+     */
+    public void setDocuments(@NonNull List<GenericDocument> documents) {
+        mDocuments = Preconditions.checkNotNull(documents);
+        notifyDataSetChanged();
+    }
+
+    @NonNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View view = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.adapter_document_list_item, parent, /*attachToRoot=*/false);
+        return new ViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+        String namespace = mDocuments.get(position).getNamespace();
+        String id = mDocuments.get(position).getId();
+        holder.getNamespaceLabel().setText(
+                "Namespace: \"" + namespace + "\"");
+        holder.getIdLabel().setText("ID: \"" + id + "\"");
+
+        holder.itemView.setOnClickListener(unusedView -> {
+                    DocumentFragment documentFragment =
+                            DocumentFragment.createDocumentFragment(namespace, id);
+                    mDocumentListFragment.getActivity().getSupportFragmentManager()
+                            .beginTransaction()
+                            .replace(R.id.fragment_container, documentFragment)
+                            .addToBackStack(/*name=*/null)
+                            .commit();
+                }
+        );
+    }
+
+    @Override
+    public int getItemCount() {
+        return mDocuments.size();
+    }
+
+    /**
+     * ViewHolder for {@link DocumentListItemAdapter}.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        private final TextView mNamespaceLabel;
+        private final TextView mIdLabel;
+
+        public ViewHolder(@NonNull View view) {
+            super(view);
+
+            Preconditions.checkNotNull(view);
+
+            mNamespaceLabel = view.findViewById(R.id.doc_item_namespace);
+            mIdLabel = view.findViewById(R.id.doc_item_id);
+        }
+
+        @NonNull
+        public TextView getNamespaceLabel() {
+            return mNamespaceLabel;
+        }
+
+        @NonNull
+        public TextView getIdLabel() {
+            return mIdLabel;
+        }
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/MenuFragment.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/MenuFragment.java
new file mode 100644
index 0000000..c7abf48
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/MenuFragment.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.debugview.R;
+import androidx.fragment.app.Fragment;
+
+/**
+ * A fragment for displaying page navigation shortcuts of the debug view.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class MenuFragment extends Fragment {
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_menu, container, /*attachToRoot=*/false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        Button documentListButton = getView().findViewById(R.id.view_documents_button);
+        documentListButton.setOnClickListener(
+                unusedView -> {
+                    DocumentListFragment documentListFragment = new DocumentListFragment();
+                    navigateToFragment(documentListFragment);
+                });
+
+        Button schemaTypeListButton = getView().findViewById(R.id.view_schema_types_button);
+        schemaTypeListButton.setOnClickListener(
+                unusedView -> {
+                    SchemaTypeListFragment schemaTypeListFragment = new SchemaTypeListFragment();
+                    navigateToFragment(schemaTypeListFragment);
+                });
+    }
+
+    private void navigateToFragment(Fragment fragment) {
+        getActivity().getSupportFragmentManager()
+                .beginTransaction()
+                .replace(R.id.fragment_container, fragment)
+                .addToBackStack(/*name=*/null)
+                .commit();
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/SchemaTypeListFragment.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/SchemaTypeListFragment.java
new file mode 100644
index 0000000..9bc7100
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/SchemaTypeListFragment.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.appsearch.debugview.R;
+import androidx.appsearch.debugview.model.SchemaTypeListModel;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.Collections;
+
+/**
+ * A fragment for displaying a list of {@link AppSearchSchema} objects.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SchemaTypeListFragment extends Fragment {
+    private static final String TAG = "AppSearchSchemaTypeFrag";
+
+    private TextView mLoadingView;
+    private TextView mEmptySchemaTypesView;
+    private TextView mSchemaVersionView;
+    private RecyclerView mSchemaTypeListRecyclerView;
+    private LinearLayoutManager mLinearLayoutManager;
+    private SchemaTypeListItemAdapter mSchemaTypeListItemAdapter;
+    private ListeningExecutorService mExecutor;
+    private ListenableFuture<DebugAppSearchManager> mDebugAppSearchManager;
+    private AppSearchDebugActivity mAppSearchDebugActivity;
+
+    @Nullable
+    protected SchemaTypeListModel mSchemaTypeListModel;
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_schema_type_list, container, /*attachToRoot=*/
+                false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        mLoadingView = getView().findViewById(R.id.loading_schema_types_text_view);
+        mEmptySchemaTypesView = getView().findViewById(R.id.empty_schema_types_text_view);
+        mSchemaTypeListRecyclerView = getView().findViewById(R.id.schema_type_list_recycler_view);
+        mSchemaVersionView = getView().findViewById(R.id.schema_version_view);
+
+        mAppSearchDebugActivity = (AppSearchDebugActivity) getActivity();
+        mExecutor = mAppSearchDebugActivity.getBackgroundExecutor();
+        mDebugAppSearchManager = mAppSearchDebugActivity.getDebugAppSearchManager();
+
+        initSchemaTypeListRecyclerView();
+
+        Futures.addCallback(mDebugAppSearchManager,
+                new FutureCallback<DebugAppSearchManager>() {
+                    @Override
+                    public void onSuccess(DebugAppSearchManager debugAppSearchManager) {
+                        readSchema(debugAppSearchManager);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Toast.makeText(getContext(),
+                                "Failed to initialize AppSearch: " + t.getMessage(),
+                                Toast.LENGTH_LONG).show();
+                        Log.e(TAG, "Failed to initialize AppSearch. Verify that the database name "
+                                + "has been provided in the intent with key: databaseName", t);
+                    }
+                }, ContextCompat.getMainExecutor(mAppSearchDebugActivity));
+    }
+
+    /**
+     * Initializes a {@link SchemaTypeListModel} ViewModel instance and sets observer for updating
+     * UI with the schema.
+     */
+    protected void readSchema(@NonNull DebugAppSearchManager debugAppSearchManager) {
+        mSchemaTypeListModel =
+                new ViewModelProvider(this,
+                        new SchemaTypeListModel.SchemaTypeListModelFactory(mExecutor,
+                                debugAppSearchManager)).get(SchemaTypeListModel.class);
+
+        mSchemaTypeListModel.getSchemaTypes().observe(this, schemaTypeList -> {
+            mLoadingView.setVisibility(View.GONE);
+
+            if (schemaTypeList.size() == 0) {
+                mEmptySchemaTypesView.setVisibility(View.VISIBLE);
+                mSchemaTypeListRecyclerView.setVisibility(View.GONE);
+            } else {
+                mSchemaTypeListItemAdapter.setSchemaTypes(schemaTypeList);
+            }
+        });
+
+        mSchemaTypeListModel.getSchemaVersion().observe(this, version -> {
+            mSchemaVersionView.setText(
+                    getString(R.string.appsearch_schema_version, version));
+            mSchemaVersionView.setVisibility(View.VISIBLE);
+        });
+    }
+
+    private void initSchemaTypeListRecyclerView() {
+        mLinearLayoutManager = new LinearLayoutManager(mAppSearchDebugActivity);
+        mLinearLayoutManager.setOrientation(RecyclerView.VERTICAL);
+
+        mSchemaTypeListItemAdapter = new SchemaTypeListItemAdapter(Collections.emptyList());
+
+        mSchemaTypeListRecyclerView.setAdapter(mSchemaTypeListItemAdapter);
+        mSchemaTypeListRecyclerView.setLayoutManager(mLinearLayoutManager);
+        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
+                mAppSearchDebugActivity, mLinearLayoutManager.getOrientation());
+        mSchemaTypeListRecyclerView.addItemDecoration(dividerItemDecoration);
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/SchemaTypeListItemAdapter.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/SchemaTypeListItemAdapter.java
new file mode 100644
index 0000000..277b050
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/SchemaTypeListItemAdapter.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.debugview.R;
+import androidx.core.util.Preconditions;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * Adapter for displaying a list of {@link AppSearchSchema} objects.
+ *
+ * <p>This adapter displays each schema type with its name.
+ *
+ * <p>Schema types can be manually changed by calling {@link #setSchemaTypes}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SchemaTypeListItemAdapter extends
+        RecyclerView.Adapter<SchemaTypeListItemAdapter.ViewHolder> {
+    private List<AppSearchSchema> mSchemaTypes;
+
+    SchemaTypeListItemAdapter(@NonNull List<AppSearchSchema> schemaTypes) {
+        mSchemaTypes = Preconditions.checkNotNull(schemaTypes);
+    }
+
+    /**
+     * Sets the adapter's schema type list.
+     *
+     * @param schemaTypes list of {@link AppSearchSchema} objects to update adapter with.
+     */
+    public void setSchemaTypes(@NonNull List<AppSearchSchema> schemaTypes) {
+        mSchemaTypes = Preconditions.checkNotNull(schemaTypes);
+        notifyDataSetChanged();
+    }
+
+    @NonNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View view = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.adapter_schema_type_list_item, parent, /*attachToRoot=*/false);
+        return new ViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+        String schemaType = mSchemaTypes.get(position).getSchemaType();
+
+        holder.getSchemaTypeLabel().setText(schemaType);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mSchemaTypes.size();
+    }
+
+    /**
+     * ViewHolder for {@link SchemaTypeListItemAdapter}.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        private final TextView mSchemaTypeLabel;
+
+        public ViewHolder(@NonNull View view) {
+            super(view);
+
+            Preconditions.checkNotNull(view);
+
+            mSchemaTypeLabel = view.findViewById(R.id.schema_type_item_title);
+        }
+
+        @NonNull
+        public TextView getSchemaTypeLabel() {
+            return mSchemaTypeLabel;
+        }
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/ScrollListener.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/ScrollListener.java
new file mode 100644
index 0000000..a2dd0e98
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/ScrollListener.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Listens for scrolling and loads the next page of results if the end of the view is reached.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public abstract class ScrollListener extends RecyclerView.OnScrollListener {
+    private final LinearLayoutManager mLayoutManager;
+
+    public ScrollListener(@NonNull LinearLayoutManager layoutManager) {
+        mLayoutManager = Preconditions.checkNotNull(layoutManager);
+    }
+
+    @Override
+    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+        super.onScrolled(recyclerView, dx, dy);
+
+        int itemsVisible = mLayoutManager.getChildCount();
+        int totalItems = mLayoutManager.getItemCount();
+        int firstItemInViewIndex = mLayoutManager.findFirstVisibleItemPosition();
+
+        // This value is true when the RecyclerView has additional rows that can be filled and
+        // the underlying adapter does not have sufficient items to fill them.
+        boolean hasAdditionalRowsToFill = (firstItemInViewIndex + itemsVisible) >= totalItems;
+
+        if (!isLoading() && hasAdditionalPages()) {
+            if (hasAdditionalRowsToFill && firstItemInViewIndex >= 0) {
+                loadNextPage();
+            }
+        }
+    }
+
+    /**
+     * Defines how to load the next page of results to display.
+     */
+    public abstract void loadNextPage();
+
+    /**
+     * Indicates whether a page is currently be loading.
+     *
+     * <p>{@link #loadNextPage()} will not be called if this is {@code true}.
+     */
+    public abstract boolean isLoading();
+
+    /**
+     * Indicates whether there are additional pages to load.
+     *
+     * <p>{@link #loadNextPage()} will not be called if this is {@code true}.
+     */
+    public abstract boolean hasAdditionalPages();
+}
diff --git a/appsearch/debug-view/src/main/res/layout/activity_appsearchdebug.xml b/appsearch/debug-view/src/main/res/layout/activity_appsearchdebug.xml
new file mode 100644
index 0000000..ca53b16
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/activity_appsearchdebug.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".view.AppSearchDebugActivity" >
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/appsearch/debug-view/src/main/res/layout/adapter_document_list_item.xml b/appsearch/debug-view/src/main/res/layout/adapter_document_list_item.xml
new file mode 100644
index 0000000..7060743
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/adapter_document_list_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="2px"
+    android:minHeight="42px" >
+
+    <TextView
+        android:id="@+id/doc_item_namespace"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <TextView
+        android:id="@+id/doc_item_id"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/appsearch/debug-view/src/main/res/layout/adapter_schema_type_list_item.xml b/appsearch/debug-view/src/main/res/layout/adapter_schema_type_list_item.xml
new file mode 100644
index 0000000..2d60d60
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/adapter_schema_type_list_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"
+    android:gravity="center"
+    android:orientation="vertical"
+    android:padding="4dp" >
+
+    <TextView
+        android:id="@+id/schema_type_item_title"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/appsearch/debug-view/src/main/res/layout/fragment_document.xml b/appsearch/debug-view/src/main/res/layout/fragment_document.xml
new file mode 100644
index 0000000..22b8d67
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/fragment_document.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".view.DocumentFragment"
+    android:padding="6px" >
+
+    <TextView
+        android:id="@+id/document_string"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Loading document..." />
+
+</LinearLayout>
diff --git a/appsearch/debug-view/src/main/res/layout/fragment_document_list.xml b/appsearch/debug-view/src/main/res/layout/fragment_document_list.xml
new file mode 100644
index 0000000..78d2431
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/fragment_document_list.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".view.AppSearchDebugActivity" >
+
+    <TextView
+        android:id="@+id/loading_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="@string/appsearch_documents_loading"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/document_list_recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/empty_documents_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="@string/appsearch_no_documents_error"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/appsearch/debug-view/src/main/res/layout/fragment_menu.xml b/appsearch/debug-view/src/main/res/layout/fragment_menu.xml
new file mode 100644
index 0000000..e57a8bc9
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/fragment_menu.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    tools:context=".view.MenuFragment">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:gravity="center_horizontal" >
+
+        <Button
+            android:id="@+id/view_documents_button"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:text="VIEW DOCUMENTS" />
+
+        <Button
+            android:id="@+id/view_schema_types_button"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:text="VIEW SCHEMA TYPES" />
+
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/appsearch/debug-view/src/main/res/layout/fragment_schema_type_list.xml b/appsearch/debug-view/src/main/res/layout/fragment_schema_type_list.xml
new file mode 100644
index 0000000..5b85784
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/fragment_schema_type_list.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".view.AppSearchDebugActivity" >
+
+    <TextView
+        android:id="@+id/loading_schema_types_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="@string/appsearch_schema_types_loading"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/schema_version_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:text="@string/appsearch_schema_version"
+        android:minHeight="64px"
+        android:textFontWeight="700"
+        android:background="@color/design_default_color_primary_dark"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/schema_type_list_recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/empty_schema_types_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="@string/appsearch_no_schema_types_error"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/appsearch/debug-view/src/main/res/values-af/strings.xml b/appsearch/debug-view/src/main/res/values-af/strings.xml
new file mode 100644
index 0000000..51f5f48
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-af/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Geen AppSearch-dokumente in databasis gekry nie. Verifieer dat die databasisnaam geldig is en die regte bergingtipe gekies is."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Geen AppSearch-skematipes gekry nie. Verifieer dat die databasisnaam geldig is en die regte bergingtipe gekies is."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Laai tans AppSearch-dokumente …"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Laai tans AppSearch-skematipes …"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Skemaweergawe: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-am/strings.xml b/appsearch/debug-view/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000..cb135d5
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-am/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"በመረጃ ጎታ ውስጥ ምንም የመተግበሪያ ፍለጋ ሰነዶች አልተገኙም። የመረጃ ጎታ ስም ትክክለኛ መሆኑን እና ትክክለኛው የማከማቻ ዓይነት መመረጡን ያረጋግጡ።"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"ምንም የመተግበሪያ ፍለጋ የንድፍ አይነቶች አልተገኙም። የመረጃ ጎታ ስም ትክክለኛ መሆኑን እና ትክክለኛው የማከማቻ ዓይነት መመረጡን ያረጋግጡ።"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"የመተግበሪያ ፍለጋ ሰነዶችን በመጫን ላይ..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"የመተግበሪያ ፍለጋ የንድፍ ዓይነቶችን በመጫን ላይ..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"የንድፍ ስሪት፦ %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ar/strings.xml b/appsearch/debug-view/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..ea253a8
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ar/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"‏لم يتم العثور على مستندات AppSearch في قاعدة البيانات. تأكّد من أن اسم قاعدة البيانات صالح ومن اختيار نوع وحدة تخزين صحيح."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"‏لم يتم العثور على أنواع مخططات AppSearch. تأكّد من أن اسم قاعدة البيانات صالح ومن اختيار نوع وحدة تخزين صحيح."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"‏جارٍ تحميل مستندات AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"‏جارٍ تحميل أنواع مخططات AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"‏إصدار المخطط: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-as/strings.xml b/appsearch/debug-view/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..a2d4396
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-as/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ডেটাবেছত কোনো AppSearchৰ নথি পোৱা নগ’ল। সত্যাপন কৰক যে ডেটাবেছৰ নামটো মান্য আৰু ষ্ট’ৰেজৰ সঠিক প্ৰকাৰটো বাছনি কৰা হৈছে।"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearchৰ স্কীমাৰ কোনো প্ৰকাৰ পোৱা নগ’ল। সত্যাপন কৰক যে ডেটাবেছৰ নামটো মান্য আৰু ষ্ট’ৰেজৰ সঠিক প্ৰকাৰটো বাছনি কৰা হৈছে।"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearchৰ নথি ল’ড কৰি থকা হৈছে..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearchৰ স্কীমাৰ প্ৰকাৰ ল’ড কৰি থকা হৈছে..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"স্কীমাৰ সংস্কৰণ: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-az/strings.xml b/appsearch/debug-view/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000..b7744fe
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-az/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Data bazasında AppSearch sənədləri tapılmadı. Data bazası adının doğru olduğunu və düzgün yaddaş növünün seçildiyini doğrulayın."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch sxem növləri tapılmadı. Data bazası adının doğru olduğunu və düzgün yaddaş növünün seçildiyini doğrulayın."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch sənədləri yüklənir..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch sxem növləri yüklənir..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Sxem Versiyası: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-b+sr+Latn/strings.xml b/appsearch/debug-view/src/main/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..b1344d4
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Nije pronađet nijedan AppSearch dokument u bazi podataka. Verifikujte da je baza podataka važeća i da je izabran ispravan tip memorijskog prostora."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nije pronađen nijedan tip AppSearch šeme. Verifikujte da je baza podataka važeća i da je izabran ispravan tip memorijskog prostora."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Učitavaju se AppSearch dokumenti..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Tipovi AppSearch šeme se učitavaju..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Verzija šeme: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-be/strings.xml b/appsearch/debug-view/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000..461f4e4
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-be/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Дакументы AppSearch не знойдзены ў базе даных. Пераканайцеся ў тым, што вы ўвялі сапраўдную назву базы даных і выбралі правільны тып сховішча."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Тыпы схемы AppSearch не знойдзены. Пераканайцеся ў тым, што вы ўвялі сапраўдную назву базы даных і выбралі правільны тып сховішча."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Загрузка дакументаў AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Загрузка тыпаў схемы AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Версія схемы: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-bg/strings.xml b/appsearch/debug-view/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000..8773eb6
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-bg/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"В базата от данни не бяха намерени документи от AppSearch. Уверете се, че името на базата от данни е валидно и сте избрали правилния тип на хранилището."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Не бяха открити типове схема за AppSearch. Уверете се, че името на базата от данни е валидно и сте избрали правилния тип на хранилището."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Документите от AppSearch се зареждат…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Типовете схема за AppSearch се зареждат…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Версия на схемата: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-bn/strings.xml b/appsearch/debug-view/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000..0ff4244
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-bn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ডেটাবেসে AppSearch-এর কোনও ডকুমেন্ট খুঁজে পাওয়া যায়নি। ডেটাবেসের নাম ঠিক আছে এবং স্টোরেজের সঠিক ধরন সঠিক বেছে নেওয়া হয়েছে কিনা তা যাচাই করুন।"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"কোনও AppSearch স্কিমা খুঁজে পাওয়া যায়নি। ডেটাবেসের নাম ঠিক আছে এবং স্টোরেজের সঠিক ধরন সঠিক বেছে নেওয়া হয়েছে কিনা তা যাচাই করুন।"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch-এর ডকুমেন্ট লোড করা হচ্ছে..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"বিভিন্ন ধরনের AppSearch স্কিমা লোড করা হচ্ছে..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"স্কিমা ভার্সন: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-bs/strings.xml b/appsearch/debug-view/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000..5c643b0
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-bs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Nijedan dokument na usluzi AppSearch nije pronađen u bazi podataka. Potvrdite da je naziv baze podataka važeći i da je odabrana ispravna vrsta pohrane."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nije pronađena nijedna vrsta šema na usluzi AppSearch. Potvrdite da je naziv baze podataka važeći i da je odabrana ispravna vrsta pohrane."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Učitavanje dokumenata na usluzi AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Učitavanje vrsta šema na usluzi AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Verzija šeme: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ca/strings.xml b/appsearch/debug-view/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000..6e81fa0
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ca/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"No s\'ha trobat cap document d\'AppSearch a la base de dades. Comprova que el nom de la base de dades sigui vàlid i que hagis seleccionat el tipus d\'emmagatzematge correcte."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"No s\'ha trobat cap tipus d\'esquema d\'AppSearch. Comprova que el nom de la base de dades sigui vàlid i que hagis seleccionat el tipus d\'emmagatzematge correcte."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"S\'estan carregant els documents d\'AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"S\'estan carregant els tipus d\'esquema d\'AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versió de l\'esquema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-cs/strings.xml b/appsearch/debug-view/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000..162dfa0
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-cs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"V databázi nebyly nalezeny žádné dokumenty AppSearch. Zkontrolujte, zda je název databáze platný a zda byl vybrán správný typ úložiště."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nebyly nalezeny žádné typy schémat AppSearch. Zkontrolujte, zda je název databáze platný a zda byl vybrán správný typ úložiště."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Načítání dokumentů AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Načítání typů schémat AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Verze schématu: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-da/strings.xml b/appsearch/debug-view/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000..54bccbc
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-da/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Der blev ikke fundet nogen AppSearch-dokumenter i databasen. Bekræft, at databasens navn er gyldigt, og at du har valgt den korrekte lagertype."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Der blev ikke fundet nogen AppSearch-skematyper. Bekræft, at databasens navn er gyldigt, og at du har valgt den korrekte lagertype."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Indlæser AppSearch-dokumenter…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Indlæser AppSearch-skematyper…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Skemaversion: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-de/strings.xml b/appsearch/debug-view/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000..04b2784
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-de/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"In der Datenbank wurden keine AppSearch-Dokumente gefunden. Überprüfe, ob der Name der Datenbank gültig ist und der richtige Speichertyp ausgewählt wurde."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Es wurden keine AppSearch-Schematypen gefunden. Überprüfe, ob der Name der Datenbank gültig ist und der richtige Speichertyp ausgewählt wurde."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch-Dokumente werden geladen…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch-Schematypen werden geladen…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schemaversion: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-el/strings.xml b/appsearch/debug-view/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000..11e3c8b
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-el/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Δεν βρέθηκαν έγγραφα AppSearch στη βάση δεδομένων. Επαληθεύστε ότι το όνομα της βάσης δεδομένων είναι έγκυρο και ότι έχει επιλεγεί ο σωστός τύπος αποθηκευτικού χώρου."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Δεν βρέθηκαν τύποι σχήματος AppSearch. Επαληθεύστε ότι το όνομα της βάσης δεδομένων είναι έγκυρο και ότι έχει επιλεγεί ο σωστός τύπος αποθηκευτικού χώρου."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Φόρτωση εγγράφων AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Φόρτωση τύπων σχήματος AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Έκδοση σχήματος: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-en-rAU/strings.xml b/appsearch/debug-view/src/main/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..81d3a2d
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-en-rAU/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"No AppSearch documents found in database. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"No AppSearch schema types found. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Loading AppSearch documents…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Loading AppSearch schema types…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schema version: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-en-rCA/strings.xml b/appsearch/debug-view/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..81d3a2d
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"No AppSearch documents found in database. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"No AppSearch schema types found. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Loading AppSearch documents…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Loading AppSearch schema types…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schema version: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-en-rGB/strings.xml b/appsearch/debug-view/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..81d3a2d
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"No AppSearch documents found in database. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"No AppSearch schema types found. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Loading AppSearch documents…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Loading AppSearch schema types…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schema version: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-en-rIN/strings.xml b/appsearch/debug-view/src/main/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..81d3a2d
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-en-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"No AppSearch documents found in database. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"No AppSearch schema types found. Verify that the database name is valid and that the correct storage type was selected."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Loading AppSearch documents…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Loading AppSearch schema types…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schema version: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-en-rXC/strings.xml b/appsearch/debug-view/src/main/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..b65f0b8
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-en-rXC/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‎‏‎‏‎‏‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‎‎‏‏‎‏‎‎‎‎‎‏‏‎‎‎‎‏‎‎‏‎‏‎‏‎No AppSearch documents found in database. Verify that the database name is valid and the correct storage type was selected.‎‏‎‎‏‎"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‎‏‎‏‎‏‏‎‎‏‎‏‎‎‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎‎‏‎‏‎‏‏‏‏‎‎No AppSearch schema types found. Verify that the database name is valid and the correct storage type was selected.‎‏‎‎‏‎"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‎‎‏‏‎‏‎‏‏‏‏‏‎‎‏‏‎‏‏‏‏‎‏‏‏‏‏‎‎‏‏‏‎‎‎‏‏‏‎‎‎‏‏‎‎‏‎‎Loading AppSearch documents...‎‏‎‎‏‎"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‎‏‏‏‎‏‏‏‎‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‎‎‏‏‏‎‎‏‏‏‎‎‎‎‏‎‏‏‎‎‎‏‎‎‎Loading AppSearch schema types...‎‏‎‎‏‎"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‎‎‎‎‎‏‏‏‎‏‎‏‎‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‎‎‏‎‏‏‏‎‎Schema Version: %d‎‏‎‎‏‎"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-es-rUS/strings.xml b/appsearch/debug-view/src/main/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..432a7b6
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-es-rUS/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"No se encontró ningún documento de AppSearch en la base de datos. Verifica que el nombre de la base de datos sea válido y que hayas seleccionado el tipo de almacenamiento correcto."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"No se encontró ningún tipo de esquema de AppSearch. Verifica que el nombre de la base de datos sea válido y que hayas seleccionado el tipo de almacenamiento correcto."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Cargando documentos de AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Cargando tipos de esquema de AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versión del esquema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-es/strings.xml b/appsearch/debug-view/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000..b37ba84b
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-es/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"No se ha encontrado ningún documento de AppSearch en la base de datos. Comprueba que el nombre de la base de datos sea válido y que esté seleccionado el tipo de almacenamiento correcto."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"No se han encontrado ningún tipo de esquema de AppSearch. Comprueba que el nombre de la base de datos sea válido y que esté seleccionado el tipo de almacenamiento correcto."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Cargando documentos de AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Cargando tipos de esquema de AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versión del esquema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-et/strings.xml b/appsearch/debug-view/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000..bbe743b
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-et/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Andmebaasist ei leitud ühtegi AppSearchi dokumenti. Veenduge, et andmebaasi nimi oleks kehtiv ja valitud oleks õige salvestusruumi tüüp."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Ühtegi AppSearchi skeemi tüüpi ei leitud. Veenduge, et andmebaasi nimi oleks kehtiv ja valitud oleks õige salvestusruumi tüüp."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearchi dokumentide laadimine …"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearchi skeemi tüüpide laadimine …"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Skeemi versioon: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-eu/strings.xml b/appsearch/debug-view/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000..757465d
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-eu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Ez da aurkitu AppSearch-eko dokumenturik datu-basean. Egiaztatu datu-basearen izenak balio duela eta biltegiratze mota zuzena hautatu dela."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Ez da aurkitu AppSearch-en eskema motarik. Egiaztatu datu-basearen izenak balio duela eta biltegiratze mota zuzena hautatu dela."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch-eko dokumentuak kargatzen…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch-en eskema motak kargatzen…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Eskemaren bertsioa: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-fa/strings.xml b/appsearch/debug-view/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000..48e62ff
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-fa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"‏هیچ سندی از AppSearch در پایگاه داده پیدا نشد. تأیید کنید که نام پایگاه داده معتبر است و نوع صحیح فضای ذخیره‌سازی انتخاب شده است."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"‏هیچ نوعی از طرح AppSearch پیدا نشد. تأیید کنید که نام پایگاه داده معتبر است و نوع صحیح فضای ذخیره‌سازی انتخاب شده است."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"‏درحال بار کردن اسناد AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"‏درحال بار کردن انواع طرح AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"‏نسخه طرح: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-fi/strings.xml b/appsearch/debug-view/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..3fda0ea
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-fi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"AppSearchin dokumentteja ei löytynyt tietokannasta. Varmista, että tietokannan nimi on oikea ja että olet valinnut oikean tallennustilatyypin."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearchin mallityyppejä ei löytynyt tietokannasta. Varmista, että tietokannan nimi on oikea ja että olet valinnut oikean tallennustilatyypin."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Ladataan AppSearchin dokumentteja…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Ladataan AppSearchin mallityyppejä…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Mallin versio: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-fr-rCA/strings.xml b/appsearch/debug-view/src/main/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..b306b21
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-fr-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Aucun document AppSearch trouvé dans la base de données. Vérifiez que le nom de la base de données est correct et que le bon type d\'espace de stockage a été sélectionné."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Aucun type de schéma AppSearch trouvé. Vérifiez que le nom de la base de données est correct et que le bon type d\'espace de stockage a été sélectionné."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Chargement des documents AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Chargement des types de schémas AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Version du schéma : %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-fr/strings.xml b/appsearch/debug-view/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000..96553cc
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-fr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Aucun document AppSearch trouvé dans la base de données. Vérifiez que le nom de la base de données est valide et que vous avez sélectionné le type d\'espace de stockage correct."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Aucun type de schéma AppSearch trouvé. Vérifiez que le nom de la base de données est valide et que vous avez sélectionné le type d\'espace de stockage correct."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Chargement des documents AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Chargement des types de schémas AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Version du schéma : %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-gl/strings.xml b/appsearch/debug-view/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000..ce3ecf9
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-gl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Non se atoparon documentos de AppSearch na base de datos. Comproba que o nome da base de datos sexa válido e que se seleccionase o tipo de almacenamento correcto."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Non se atoparon tipos de esquemas de AppSearch. Comproba que o nome da base de datos sexa válido e que se seleccionase o tipo de almacenamento correcto."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Cargando documentos de AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Cargando tipos de esquemas de AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versión do esquema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-gu/strings.xml b/appsearch/debug-view/src/main/res/values-gu/strings.xml
new file mode 100644
index 0000000..4aeaffa
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-gu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ડેટાબેઝમાં AppSearchના કોઈ દસ્તાવેજ મળ્યા નથી. ડેટાબેઝનું નામ માન્ય હોવાની અને પસંદ કરેલા સ્ટોરેજનો પ્રકાર યોગ્ય હોવાની ચકાસણી કરો."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch સ્કીમાના કોઈ પ્રકાર મળ્યા નથી. ડેટાબેઝનું નામ માન્ય હોવાની અને પસંદ કરેલા સ્ટોરેજનો પ્રકાર યોગ્ય હોવાની ચકાસણી કરો."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearchના દસ્તાવેજો લોડ કરી રહ્યાં છીએ…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch સ્કીમાના પ્રકારો લોડ કરી રહ્યાં છીએ…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"સ્કીમા વર્ઝન: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-hi/strings.xml b/appsearch/debug-view/src/main/res/values-hi/strings.xml
new file mode 100644
index 0000000..c2c5525
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-hi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"डेटाबेस में, AppSearch का कोई दस्तावेज़ नहीं मिला. पुष्टि करें कि डेटाबेस का नाम मान्य है और डिवाइस के स्टोरेज का सही टाइप चुना गया है."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch का कोई स्कीमा टाइप नहीं मिला. पुष्टि करें कि डेटाबेस का नाम मान्य है और डिवाइस के स्टोरेज का सही टाइप चुना गया है."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch के दस्तावेज़ लोड हो रहे हैं..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch के स्कीमा टाइप लोड हो रहे हैं..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"स्कीमा वर्शन %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-hr/strings.xml b/appsearch/debug-view/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000..0ebf67a4
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-hr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"U bazi podataka nije pronađen nijedan AppSearch dokument. Potvrdite da je naziv baze podataka važeći i da je odabrana točna vrsta pohrane."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nije pronađena nijedna vrsta AppSearch sheme. Potvrdite da je naziv baze podataka važeći i da je odabrana točna vrsta pohrane."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Učitavanje AppSearch dokumenata..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Učitavanje vrsta AppSearch shema..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Verzija sheme: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-hu/strings.xml b/appsearch/debug-view/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000..d6c2de3
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-hu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Nem találhatók Alkalmazáskereső-dokumentumok az adatbázisban. Ellenőrizze az adatbázis nevének érvényességét és a kiválasztott tárhelytípus helyességét."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nem találhatók Alkalmazáskereső-sématípusok. Ellenőrizze az adatbázis nevének érvényességét és a kiválasztott tárhelytípus helyességét."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Alkalmazáskereső-dokumentumok betöltése…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Alkalmazáskereső-sématípusok betöltése…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Séma-verziószám: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-hy/strings.xml b/appsearch/debug-view/src/main/res/values-hy/strings.xml
new file mode 100644
index 0000000..f078b38
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-hy/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Տվյալների շտեմարանում AppSearch-ի փաստաթղթեր չեն գտնվել։ Համոզվեք, որ տվյալների շտեմարանի անունը վավեր է և հիշողության ճիշտ տեսակն է ընտրված։"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch-ի սխեմաների տեսակներ չեն գտնվել։ Համոզվեք, որ տվյալների շտեմարանի անունը վավեր է և հիշողության ճիշտ տեսակն է ընտրված։"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch-ի փաստաթղթերը բեռնվում են..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch-ի սխեմաների տեսակները բեռնվում են..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Սխեմայի տարբերակը՝ %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-in/strings.xml b/appsearch/debug-view/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000..f01fd3c
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-in/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Tidak ada dokumen AppSearch yang ditemukan di database. Pastikan nama database valid dan jenis penyimpanan yang tepat telah dipilih."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Tidak ada jenis skema AppSearch yang ditemukan. Pastikan nama database valid dan jenis penyimpanan yang tepat telah dipilih."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Memuat dokumen AppSearch ..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Memuat jenis skema AppSearch ..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versi Skema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-is/strings.xml b/appsearch/debug-view/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000..81160a7
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-is/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Engin skjöl forritaleitar fundust í gagnagrunni. Staðfestu að heiti gagnagrunns sé gilt og að rétt geymslugerð sé valin."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Engar skemagerðir forritaleitar fundust. Staðfestu að heiti gagnagrunns sé gilt og að rétt geymslugerð sé valin."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Hleður skjölum forritaleitar..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Hleður skemagerðum forritaleitar..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Útgáfa skema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-it/strings.xml b/appsearch/debug-view/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000..2924f40
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-it/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Nessun documento AppSearch trovato nel database. Verifica che il nome del database sia valido e di aver selezionato il tipo di spazio di archiviazione corretto."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nessun tipo di schema AppSearch trovato. Verifica che il nome del database sia valido e di aver selezionato il tipo di spazio di archiviazione corretto."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Caricamento dei documenti AppSearch in corso…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Caricamento dei tipi di schemi AppSearch in corso…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versione schema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-iw/strings.xml b/appsearch/debug-view/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000..0db6e25
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-iw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"‏לא נמצאו מסמכי AppSearch במסד הנתונים. צריך לוודא שהשם של מסד הנתונים תקין ושבחרת בסוג האחסון הנכון."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"‏לא נמצאו סוגי סכימה של AppSearch. צריך לוודא שהשם של מסד הנתונים תקין ושבחרת בסוג האחסון הנכון."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"‏מסמכי AppSearch נטענים…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"‏סוגי הסכימה של AppSearch נטענים…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"‏גרסת הסכימה: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ja/strings.xml b/appsearch/debug-view/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000..f5fad78
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ja/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"データベースに AppSearch ドキュメントはありませんでした。データベース名が有効であり、正しいストレージのタイプが選択されていることをご確認ください。"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch スキーマタイプは見つかりませんでした。データベース名が有効であり、正しいストレージのタイプが選択されていることをご確認ください。"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch ドキュメントを読み込んでいます..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch スキーマタイプを読み込んでいます..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"スキーマ バージョン: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ka/strings.xml b/appsearch/debug-view/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000..ec7b4d4
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ka/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"AppSearch დოკუმენტები მონაცემთა ბაზაში ვერ მოიძებნა. დაადასტურეთ, რომ მონაცემთა ბაზის სახელი სწორია და არჩეულია მეხსიერების საჭირო ტიპი."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch-ის სქემის ტიპი ვერ მოიძებნა. დაადასტურეთ, რომ მონაცემთა ბაზის სახელი სწორია და არჩეულია მეხსიერების საჭირო ტიპი."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"მიმდინარეობს AppSearch დოკუმენტების ჩატვირთვა..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"მიმდინარეობს AppSearch-ის სქემის ტიპების ჩატვირთვა..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"სქემის ვერსია: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-kk/strings.xml b/appsearch/debug-view/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000..150203b
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-kk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Дерекқорда ешбір AppSearch құжаты табылмады. Дерекқор атауы жарамды екенін және дұрыс жад түрі таңдалғанын растаңыз."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Ешбір AppSearch схема түрі табылмады. Дерекқор атауы жарамды екенін және дұрыс жад түрі таңдалғанын растаңыз."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch құжаттары жүктелуде…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch схема түрлері жүктелуде…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Схема нұсқасы: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-km/strings.xml b/appsearch/debug-view/src/main/res/values-km/strings.xml
new file mode 100644
index 0000000..b8c14f9
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-km/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"រកមិនឃើញឯកសារ AppSearch នៅក្នុងមូលដ្ឋាន​ទិន្នន័យទេ។ ផ្ទៀងផ្ទាត់ថាឈ្មោះមូលដ្ឋាន​ទិន្នន័យមានភាពត្រឹមត្រូវ និងបានជ្រើសរើសប្រភេទទំហំ​ផ្ទុកដែលត្រឹមត្រូវ។"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"រកមិនឃើញប្រភេទគ្រោង​តាង AppSearch ទេ។ ផ្ទៀងផ្ទាត់ថាឈ្មោះមូលដ្ឋាន​ទិន្នន័យមានភាពត្រឹមត្រូវ និងបានជ្រើសរើសប្រភេទទំហំ​ផ្ទុកដែលត្រឹមត្រូវ។"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"កំពុងផ្ទុកឯកសារ AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"កំពុងផ្ទុកប្រភេទគ្រោង​តាង AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"កំណែគ្រោង​តាង៖ %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-kn/strings.xml b/appsearch/debug-view/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000..3b80458
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-kn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ಡೇಟಾಬೇಸ್‌ನಲ್ಲಿ ಯಾವುದೇ AppSearch ಡಾಕ್ಯುಮೆಂಟ್‌ಗಳು ಕಂಡುಬಂದಿಲ್ಲ. ಡೇಟಾಬೇಸ್ ಹೆಸರು ಮಾನ್ಯವಾಗಿದೆಯೆ ಮತ್ತು ಸರಿಯಾದ ಸಂಗ್ರಹಣೆಯ ಪ್ರಕಾರವನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"ಯಾವುದೇ AppSearch ರೂಪುರೇಷೆ ಪ್ರಕಾರಗಳು ಕಂಡುಬಂದಿಲ್ಲ. ಡೇಟಾಬೇಸ್ ಹೆಸರು ಮಾನ್ಯವಾಗಿದೆಯೆ ಮತ್ತು ಸರಿಯಾದ ಸಂಗ್ರಹಣೆಯ ಪ್ರಕಾರವನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆಯೆ ಎಂದು ಪರಿಶೀಲಿಸಿ."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch ಡಾಕ್ಯುಮೆಂಟ್‌ಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch ರೂಪುರೇಷೆ ಪ್ರಕಾರಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"ರೂಪುರೇಷೆ ಆವೃತ್ತಿ: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ko/strings.xml b/appsearch/debug-view/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000..906947a
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ko/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"데이터베이스에서 AppSearch 문서를 찾을 수 없습니다. 데이터베이스 이름이 유효하고 올바른 저장공간 유형이 선택되어 있는지 확인하세요."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch 스키마 유형을 찾을 수 없습니다. 데이터베이스 이름이 유효하고 올바른 저장공간 유형이 선택되어 있는지 확인하세요."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch 문서 로드 중..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch 스키마 유형 로드 중..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"스키마 버전: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ky/strings.xml b/appsearch/debug-view/src/main/res/values-ky/strings.xml
new file mode 100644
index 0000000..499749c
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ky/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Дайындар базасынан AppSearch документтери табылган жок. Дайындар базасынын аталышы жарактуу экенин жана сактагычтын түрү туура тандалганын текшериңиз."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch cхемаларынын түрлөрү табылган жок. Дайындар базасынын аталышы жарактуу экенин жана сактагычтын түрү туура тандалганын текшериңиз."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch документтери жүктөлүүдө..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch cхемаларынын түрлөрү жүктөлүүдө..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Схеманын версиясы: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-lo/strings.xml b/appsearch/debug-view/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000..f69e046
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-lo/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ບໍ່ພົບເອກະສານ AppSearch ໃນຖານຂໍ້ມູນ. ກະລຸນາຢັ້ງຢືນວ່າຊື່ຖານຂໍ້ມູນນັ້ນຖືກຕ້ອງ ແລະ ໄດ້ເລືອກປະເພດບ່ອນຈັດເກັບຂໍ້ມູນທີ່ຖືກຕ້ອງແລ້ວ."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"ບໍ່ພົບປະເພດຮູບ​ແບບ AppSearch. ກະລຸນາຢັ້ງຢືນວ່າຊື່ຖານຂໍ້ມູນນັ້ນຖືກຕ້ອງ ແລະ ໄດ້ເລືອກປະເພດບ່ອນຈັດເກັບຂໍ້ມູນທີ່ຖືກຕ້ອງແລ້ວ."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"ກຳລັງໂຫຼດເອກະສານ AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"ກຳລັງໂຫຼດປະເພດຮູບແບບ AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"ເວີຊັນຮູບແບບ: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-lt/strings.xml b/appsearch/debug-view/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000..c4f1d03
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-lt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Duomenų bazėje nerasta jokių „AppSearch“ dokumentų. Įsitikinkite, kad nurodytas tinkamas duomenų bazės pavadinimas ir kad pasirinktas tinkamas saugyklos tipas."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nerasta jokių „AppSearch“ schemos tipų. Įsitikinkite, kad nurodytas tinkamas duomenų bazės pavadinimas ir kad pasirinktas tinkamas saugyklos tipas."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Įkeliami „AppSearch“ dokumentai..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Įkeliami „AppSearch“ schemos tipai..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schemos versija: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-lv/strings.xml b/appsearch/debug-view/src/main/res/values-lv/strings.xml
new file mode 100644
index 0000000..b156b36
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-lv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Datu bāzē netika atrasts neviens AppSearch dokuments. Pārbaudiet, vai datu bāzes nosaukums ir derīgs un vai tika atlasīts pareizais krātuves veids."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Netika atrasts neviens AppSearch shēmas veids. Pārbaudiet, vai datu bāzes nosaukums ir derīgs un vai tika atlasīts pareizais krātuves veids."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Notiek AppSearch dokumentu ielāde..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Notiek AppSearch shēmu veidu ielāde..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Shēmas versija: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-mk/strings.xml b/appsearch/debug-view/src/main/res/values-mk/strings.xml
new file mode 100644
index 0000000..e9ffe20
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-mk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Нема документи за AppSearch во базата на податоци. Потврдете дека името на базата на податоци е точно и дека е избран точниот вид меморија."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Не се пронајдени видови шема за AppSearch. Потврдете дека името на базата на податоци е точно и дека е избран точниот вид меморија."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Се вчитуваат документи за AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Се вчитуваат видови шема за AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Верзија на шема: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ml/strings.xml b/appsearch/debug-view/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000..2a61573
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ml/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ഡാറ്റാബേസിൽ AppSearch ഡോക്യുമെന്റുകളൊന്നും കണ്ടെത്തിയില്ല. ഡാറ്റാബേസിന്റെ പേര് സാധുവാണെന്നും ശരിയായ സ്‌റ്റോറേജ് തരമാണ് തിരഞ്ഞെടുത്തതെന്നും പരിശോധിച്ചുറപ്പിക്കുക."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch സ്‌കീമ തരങ്ങളൊന്നും കണ്ടെത്തിയില്ല. ഡാറ്റാബേസിന്റെ പേര് സാധുവാണെന്നും ശരിയായ സ്‌റ്റോറേജ് തരമാണ് തിരഞ്ഞെടുത്തതെന്നും പരിശോധിച്ചുറപ്പിക്കുക."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch ഡോക്യുമെന്റുകൾ ലോഡ് ചെയ്യുന്നു..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch സ്‌കീമ തരങ്ങൾ ലോഡ് ചെയ്യുന്നു..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"സ്‌കീമ പതിപ്പ്: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-mn/strings.xml b/appsearch/debug-view/src/main/res/values-mn/strings.xml
new file mode 100644
index 0000000..04b5530
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-mn/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Өгөгдлийн баазаас AppSearch-н документ олдсонгүй. Өгөгдлийн нэр хүчинтэй бөгөөд хадгалах сангийн зөв төрөл сонгосон эсэхийг баталгаажуулна уу."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch-н схемийн төрөл олдсонгүй. Өгөгдлийн нэр хүчинтэй бөгөөд хадгалах сангийн зөв төрөл сонгосон эсэхийг баталгаажуулна уу."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch-н документуудыг ачаалж байна..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch-н схемийн төрлийг ачаалж байна..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Схемийн хувилбар: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-mr/strings.xml b/appsearch/debug-view/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000..c0bee96
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-mr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"डेटाबेसमध्ये कोणतेही AppSearch दस्तऐवज मिळाले नाहीत. डेटाबेसचे नाव वैध असल्याची आणि योग्य स्टोरेज प्रकार निवडला असल्याची पडताळणी करा."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"कोणतेही AppSearch स्कीमा प्रकार आढळले नाहीत. डेटाबेसचे नाव वैध असल्याची आणि योग्य स्टोरेज प्रकार निवडला असल्याची पडताळणी करा."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch दस्तऐवज लोड करत आहे..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch स्कीमा प्रकार लोड करत आहे..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"स्कीमा आवृत्ती: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ms/strings.xml b/appsearch/debug-view/src/main/res/values-ms/strings.xml
new file mode 100644
index 0000000..e0396b3
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ms/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Tiada dokumen AppSearch dijumpai dalam pangkalan data. Sahkan bahawa nama pangkalan data adalah sah dan jenis storan yang dipilih adalah betul."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Jenis skema AppSearch tidak dijumpai. Sahkan bahawa nama pangkalan data adalah sah dan jenis storan yang dipilih adalah betul."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Memuatkan dokumen AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Memuatkan jenis skema AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versi Skema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-my/strings.xml b/appsearch/debug-view/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000..ae265a1
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-my/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ဒေတာဘေ့စ်တွင် AppSearch မှတ်တမ်းများ မတွေ့ပါ။ ဒေတာဘေ့စ် အမည်သည် မှန်ကန်ပြီး မှန်ကန်သော သိုလှောင်ခန်းအမျိုးအစားကို ရွေးထားကြောင်း အတည်ပြုရန် စစ်ဆေးပါ။"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch ရှေမားအမျိုးအစားများ မတွေ့ပါ။ ဒေတာဘေ့စ် အမည်သည် မှန်ကန်ပြီး မှန်ကန်သော သိုလှောင်ခန်းအမျိုးအစားကို ရွေးထားကြောင်း အတည်ပြုရန် စစ်ဆေးပါ။"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch မှတ်တမ်းများကို ဖွင့်နေသည်..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch ရှေမားအမျိုးအစားများကို ဖွင့်နေသည်..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"ရှေမား ဗားရှင်း- %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-nb/strings.xml b/appsearch/debug-view/src/main/res/values-nb/strings.xml
new file mode 100644
index 0000000..7950119
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-nb/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Fant ingen AppSearch-dokumenter i databasen. Bekreft at databasenavnet er gyldig, og at riktig lagringstype er valgt."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Fant ingen AppSearch-oppsettyper. Bekreft at databasenavnet er gyldig, og at riktig lagringstype er valgt."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Laster inn AppSearch-dokumenter …"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Laster inn AppSearch-oppsettyper …"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Oppsettversjon: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ne/strings.xml b/appsearch/debug-view/src/main/res/values-ne/strings.xml
new file mode 100644
index 0000000..f96675e
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ne/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"डेटाबेसमा कुनै पनि AppSearch डकुमेन्ट फेला परेन। डेटाबेसको नाम वैध छ र डिभाइसको सही प्रकारको भण्डारण चयन गरिएको छ भन्ने कुरा पुष्टि गर्नुहोस्।"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"कुनै पनि प्रकारको AppSearch योजना फेला परेन। डेटाबेसको नाम वैध छ र डिभाइसको सही प्रकारको भण्डारण चयन गरिएको छ भन्ने कुरा पुष्टि गर्नुहोस्।"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch डकुमेन्टहरू लोड गरिँदै छ..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"विभिन्न प्रकारका AppSearch योजना लोड गरिँदै छन्..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"योजनाको संस्करण: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-nl/strings.xml b/appsearch/debug-view/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000..2ae5bf7
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-nl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Geen AppSearch-documenten gevonden in database. Check of de databasenaam geldig is en of het juiste opslagtype is geselecteerd."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Geen AppSearch-schematypen gevonden. Check of de databasenaam geldig is en of het juiste opslagtype is geselecteerd."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch-documenten laden..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch-schematypen laden..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schemaversie: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-or/strings.xml b/appsearch/debug-view/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..a026025
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-or/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ଡାଟାବେସରେ କୌଣସି AppSearch ଡକ୍ୟୁମେଣ୍ଟ ମିଳିଲା ନାହିଁ। ଡାଟାବେସର ନାମ ବୈଧ ଥିବା ଏବଂ ସଠିକ୍ ଷ୍ଟୋରେଜ୍ ପ୍ରକାରକୁ ଚୟନ କରାଯାଇଥିବା ଯାଞ୍ଚ କରନ୍ତୁ।"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"କୌଣସି AppSearch ସ୍କିମା ପ୍ରକାର ମିଳିଲା ନାହିଁ। ଡାଟାବେସର ନାମ ବୈଧ ଥିବା ଏବଂ ସଠିକ୍ ଷ୍ଟୋରେଜ୍ ପ୍ରକାରକୁ ଚୟନ କରାଯାଇଥିବା ଯାଞ୍ଚ କରନ୍ତୁ।"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch ଡକ୍ୟୁମେଣ୍ଟଗୁଡ଼ିକ ଲୋଡ୍ ହେଉଛି..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch ସ୍କିମା ପ୍ରକାରଗୁଡ଼ିକ ଲୋଡ୍ ହେଉଛି..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"ସ୍କିମା ସଂସ୍କରଣ: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-pa/strings.xml b/appsearch/debug-view/src/main/res/values-pa/strings.xml
new file mode 100644
index 0000000..b7c0c95
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-pa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ਡਾਟਾਬੇਸ ਵਿੱਚ ਕੋਈ AppSearch ਦਸਤਾਵੇਜ਼ ਨਹੀਂ ਮਿਲੇ। ਪੁਸ਼ਟੀ ਕਰੋ ਕਿ ਡਾਟਾਬੇਸ ਦਾ ਨਾਮ ਵੈਧ ਹੈ ਅਤੇ ਸਟੋਰੇਜ ਦੀ ਇੱਕ ਸਹੀ ਕਿਸਮ ਚੁਣੀ ਗਈ ਸੀ।"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch ਸਕੀਮਾ ਦੀ ਕੋਈ ਕਿਸਮ ਨਹੀਂ ਮਿਲੀ। ਪੁਸ਼ਟੀ ਕਰੋ ਕਿ ਡਾਟਾਬੇਸ ਦਾ ਨਾਮ ਵੈਧ ਹੈ ਅਤੇ ਸਟੋਰੇਜ ਦੀ ਇੱਕ ਸਹੀ ਕਿਸਮ ਚੁਣੀ ਗਈ ਸੀ।"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch ਦਸਤਾਵੇਜ਼ਾਂ ਨੂੰ ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch ਸਕੀਮਾ ਦੀਆਂ ਕਿਸਮਾਂ ਨੂੰ ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"ਸਕੀਮਾ ਵਰਜਨ: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-pl/strings.xml b/appsearch/debug-view/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000..0cab68f
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-pl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"W bazie danych nie znaleziono dokumentów AppSearch. Sprawdź, czy nazwa bazy danych jest prawidłowa i czy wybrano odpowiedni typ pamięci."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nie znaleziono typów schematów AppSearch. Sprawdź, czy nazwa bazy danych jest prawidłowa i czy wybrano odpowiedni typ pamięci."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Wczytuję dokumenty AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Wczytuję typy schematów AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Wersja schematu: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-pt-rBR/strings.xml b/appsearch/debug-view/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..35bc29b
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Não foram encontrados documentos do AppSearch no banco de dados. Verifique se o nome do banco de dados é válido e se você selecionou o tipo de armazenamento correto."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Não foram encontrados tipos de esquema do AppSearch. Verifique se o nome do banco de dados é válido e se você selecionou o tipo de armazenamento correto."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Carregando documentos do AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Carregando tipos de esquema do AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versão do esquema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-pt-rPT/strings.xml b/appsearch/debug-view/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..a4fb83f
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Não foram encontrados documentos da AppSearch na base de dados. Valide o nome da base de dados e confirme se foi selecionado o tipo de armazenamento correto."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Não foram encontrados tipos de esquemas da AppSearch. Valide o nome da base de dados e confirme se foi selecionado o tipo de armazenamento correto."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"A carregar os documentos da AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"A carregar os tipos de esquemas da AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versão do esquema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-pt/strings.xml b/appsearch/debug-view/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000..35bc29b
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-pt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Não foram encontrados documentos do AppSearch no banco de dados. Verifique se o nome do banco de dados é válido e se você selecionou o tipo de armazenamento correto."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Não foram encontrados tipos de esquema do AppSearch. Verifique se o nome do banco de dados é válido e se você selecionou o tipo de armazenamento correto."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Carregando documentos do AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Carregando tipos de esquema do AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versão do esquema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ro/strings.xml b/appsearch/debug-view/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000..f5c4056
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ro/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Nu s-au găsit documente AppSearch în baza de date. Verificați dacă numele bazei de date este valid și s-a selectat tipul corect de stocare."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nu s-au găsit tipuri de scheme AppSearch. Verificați dacă numele bazei de date este valid și s-a selectat tipul corect de stocare."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Se încarcă documentele AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Se încarcă tipurile de scheme AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versiunea schemei: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ru/strings.xml b/appsearch/debug-view/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..0fef687
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ru/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"В базе данных нет документов AppSearch. Убедитесь, что вы правильно указали название базы данных и тип хранилища."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Типы схемы AppSearch не найдены. Убедитесь, что вы правильно указали название базы данных и тип хранилища."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Загрузка документов AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Загрузка типов схемы AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Версия схемы: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-si/strings.xml b/appsearch/debug-view/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000..37564f3
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-si/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"දත්ත සමුදායේ AppSearch ලේඛන හමු නොවීය. දත්ත සමුදායේ නම වලංගු බව සහ නිවැරදි ගබඩා වර්ගය තෝරා ගත් බව සත්‍යාපනය කරන්න."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch නිරූපණ වර්ග හමු විය. දත්ත සමුදායේ නම වලංගු බව සහ නිවැරදි ගබඩා වර්ගය තෝරා ගත් බව සත්‍යාපනය කරන්න."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch ලේඛන පූරණය කරමින්..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch නිරූපණ වර්ග පූරණය කරමින්..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"නිරූපණ අනුවාදය: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-sk/strings.xml b/appsearch/debug-view/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000..acae3cb
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-sk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"V databáze sa nenašli žiadne dokumenty služby AppSearch. Overte, či je názov databázy platný a či bol vybraný správny typ priestoru."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nenašli sa žiadne typy schém služby AppSearch. Overte, či je názov databázy platný a či bol vybraný správny typ priestoru."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Načítavajú sa dokumenty služby AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Načítavajú sa typy schém služby AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Verzia schémy: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-sl/strings.xml b/appsearch/debug-view/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000..bf2015e
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-sl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"V zbirki podatkov ni bil najden noben dokument AppSearch. Preverite, ali je ime zbirke podatkov veljavno in ali je bila izbrana prava vrsta shrambe."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Najdena ni bila nobena vrsta sheme AppSearch. Preverite, ali je ime zbirke podatkov veljavno in ali je bila izbrana prava vrsta shrambe."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Nalaganje dokumentov AppSearch …"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Nalaganje vrst shem AppSearch …"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Različica sheme: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-sq/strings.xml b/appsearch/debug-view/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000..f7e82bf
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-sq/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Nuk u gjet asnjë dokument i AppSearch në bazën e të dhënave. Verifiko që emri i bazës së të dhënave është i vlefshëm dhe që është zgjedhur lloji i duhur i hapësirës ruajtëse."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Nuk u gjet asnjë lloj i skemave së AppSearch. Verifiko që emri i bazës së të dhënave është i vlefshëm dhe që është zgjedhur lloji i duhur i hapësirës ruajtëse."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Dokumentet e AppSearch po ngarkohen..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Llojet e skemave të AppSearch po ngarkohen..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Versioni i skemës: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-sr/strings.xml b/appsearch/debug-view/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000..660dda9
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-sr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Није пронађет ниједан AppSearch документ у бази података. Верификујте да је база података важећа и да је изабран исправан тип меморијског простора."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Није пронађен ниједан тип AppSearch шеме. Верификујте да је база података важећа и да је изабран исправан тип меморијског простора."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Учитавају се AppSearch документи..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Типови AppSearch шеме се учитавају..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Верзија шеме: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-sv/strings.xml b/appsearch/debug-view/src/main/res/values-sv/strings.xml
new file mode 100644
index 0000000..df6a335
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-sv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Inget AppSearch-dokument hittades i databasen. Kontrollera att databasnamnet är giltigt och att rätt lagringstyp har angetts."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Ingen AppSearch-schematyp hittades. Kontrollera att databasnamnet är giltigt och att rätt lagringstyp har angetts."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Läser in AppSearch-dokument …"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Läser in AppSearch-schematyper …"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Schemaversion: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-sw/strings.xml b/appsearch/debug-view/src/main/res/values-sw/strings.xml
new file mode 100644
index 0000000..9750eb9
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-sw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Hakuna hati za Utafutaji wa Programu zilizopatikana katika hifadhidata. Thibitisha kuwa jina la hifadhidata ni sahihi na aina sahihi ya hifadhi imechaguliwa."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Hakuna aina za taratibu za Utafutaji wa Programu zilizopatikana. Thibitisha kuwa jina la hifadhidata ni sahihi na aina sahihi ya hifadhi imechaguliwa."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Inapakia hati za Utafutaji wa Programu..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Inapakia aina za taratibu za Utafutaji wa Programu..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Toleo la Taratibu: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ta/strings.xml b/appsearch/debug-view/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000..3d44fe9
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ta/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"தரவுத்தளத்தில் AppSearch ஆவணங்கள் எதுவுமில்லை. தரவுத்தளத்தின் பெயர் சரியானதுதான் என்பதையும் சரியான சேமிப்பக வகை தேர்ந்தெடுக்கப்பட்டுள்ளதையும் உறுதிசெய்யவும்."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch திட்டப்பணித் தரவு வகைகள் எதுவுமில்லை. தரவுத்தளத்தின் பெயர் சரியானதுதான் என்பதையும் சரியான சேமிப்பக வகை தேர்ந்தெடுக்கப்பட்டுள்ளதையும் உறுதிசெய்யவும்."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch ஆவணங்களை ஏற்றுகிறது..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch திட்டப்பணித் தரவு வகைகளை ஏற்றுகிறது..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"திட்டப்பணித் தரவுப் பதிப்பு: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-te/strings.xml b/appsearch/debug-view/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000..44a1570
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-te/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"డేటాబేస్‌లో AppSearch డాక్యుమెంట్‌లు కనుగొనబడలేదు. డేటాబేస్ పేరు చెల్లుబాటులో ఉందని వెరిఫై చేయండి, సరైన స్టోరేజ్ రకం ఎంచుకోబడింది."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch స్కీమా రకాలు కనుగొనబడలేదు. డేటాబేస్ పేరు చెల్లుబాటులో ఉందని వెరిఫై చేయండి, సరైన స్టోరేజ్ రకం ఎంచుకోబడింది."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch డాక్యుమెంట్‌లను లోడ్ చేస్తోంది..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch స్కీమా రకాలు లోడ్ అవుతున్నయి..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"స్కీమా వెర్షన్: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-th/strings.xml b/appsearch/debug-view/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000..af1bd8d
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-th/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"ไม่พบเอกสาร AppSearch ในฐานข้อมูล ตรวจสอบความถูกต้องของชื่อฐานข้อมูลและประเภทพื้นที่เก็บข้อมูลที่เลือก"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"ไม่พบประเภทสคีมาของ AppSearch ตรวจสอบความถูกต้องของชื่อฐานข้อมูลและประเภทพื้นที่เก็บข้อมูลที่เลือก"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"กำลังโหลดเอกสาร AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"กำลังโหลดประเภทสคีมาของ AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"เวอร์ชันของสคีมา: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-tl/strings.xml b/appsearch/debug-view/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000..febd912
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-tl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Walang nakitang dokumento ng AppSearch sa database. I-verify na valid ang pangalan ng database at tamang uri ng storage ang napili."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Walang nakitang uri ng schema ng AppSearch. I-verify na valid ang pangalan ng database at tamang uri ng storage ang napili."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Nilo-load ang mga dokumento ng AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Naglo-load ng mga uri ng schema ng AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Bersyon ng Schema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-tr/strings.xml b/appsearch/debug-view/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000..cb5e867
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-tr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Veritabanında AppSearch belgesi bulunamadı. Veritabanı adının geçerli olduğunu ve doğru depolama türünün seçildiğini doğrulayın."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"AppSearch şema türü bulunamadı. Veritabanı adının geçerli olduğunu ve doğru depolama türünün seçildiğini doğrulayın."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch belgeleri yükleniyor..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch şema türleri yükleniyor..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Şema Sürümü: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-uk/strings.xml b/appsearch/debug-view/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000..ce896d3
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-uk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"У базі даних немає документів AppSearch. Підтвердьте, що назва бази даних дійсна й вибрано правильний тип сховища."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Типів схеми AppSearch не знайдено. Підтвердьте, що назва бази даних дійсна й вибрано правильний тип сховища."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Завантаження документів AppSearch…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Завантаження типів схеми AppSearch…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Версія схеми: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-ur/strings.xml b/appsearch/debug-view/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000..c88f789
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-ur/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"‏ڈیٹا بیس میں کوئی AppSearch دستاویز نہیں ملا۔ توثیق کریں کہ ڈیٹا بیس کا نام درست ہے اور اسٹوریج کی صحیح قسم منتخب کی گئی تھی۔"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"‏AppSearch سکیما کی کوئی قسم نہیں ملی۔ توثیق کریں کہ ڈیٹا بیس کا نام درست ہے اور اسٹوریج کی صحیح قسم منتخب کی گئی تھی۔"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"‏AppSearch دستاویزات لوڈ ہو رہے ہیں..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"‏AppSearch سکیما کی اقسام لوڈ ہو رہی ہیں..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"‏سکیما ورژن: ‎%d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-uz/strings.xml b/appsearch/debug-view/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000..46fa912
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-uz/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Ma’lumotlar bazasidan hech qanday AppSearch hujjatlari topilmadi. Ma’lumotlar bazasi yaroqli va xatosiz xotira tanlanganini tasdiqlang."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Hech qanday AppSearch sxemasi topilmadi. Ma’lumotlar bazasi yaroqli va xatosiz xotira tanlanganini tasdiqlang."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"AppSearch hujjatlari yuklanmoqda..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"AppSearch sxemasi turlari yuklanmoqda..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Sxema versiyasi: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-vi/strings.xml b/appsearch/debug-view/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000..7024d29
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-vi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Không tìm thấy tài liệu AppSearch trong cơ sở dữ liệu. Hãy xác minh rằng tên cơ sở dữ liệu là hợp lệ và bạn đã chọn đúng loại bộ nhớ."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Không tìm thấy loại giản đồ AppSearch. Hãy xác minh rằng tên cơ sở dữ liệu là hợp lệ và bạn đã chọn đúng loại bộ nhớ."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Đang tải các tài liệu AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Đang tải các loại giản đồ AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Phiên bản giản đồ: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-zh-rCN/strings.xml b/appsearch/debug-view/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..ec6d36e
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"未在数据库中找到任何 AppSearch 文档。请确认数据库名称有效且选择的存储类型正确。"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"未找到任何 AppSearch 架构类型。请确认数据库名称有效且选择的存储类型正确。"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"正在加载 AppSearch 文档…"</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"正在加载 AppSearch 架构类型…"</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"架构版本:%d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-zh-rHK/strings.xml b/appsearch/debug-view/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..a2e6f05
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"資料庫中找不到 AppSearch 文件。請確認資料庫名稱有效,並選取正確的儲存空間類型。"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"找不到 AppSearch 結構定義類型。請確認資料庫名稱有效,並選取正確的儲存空間類型。"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"正在載入 AppSearch 文件..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"正在載入 AppSearch 結構定義類型..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"結構定義版本:%d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-zh-rTW/strings.xml b/appsearch/debug-view/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..a2e6f05
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"資料庫中找不到 AppSearch 文件。請確認資料庫名稱有效,並選取正確的儲存空間類型。"</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"找不到 AppSearch 結構定義類型。請確認資料庫名稱有效,並選取正確的儲存空間類型。"</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"正在載入 AppSearch 文件..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"正在載入 AppSearch 結構定義類型..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"結構定義版本:%d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values-zu/strings.xml b/appsearch/debug-view/src/main/res/values-zu/strings.xml
new file mode 100644
index 0000000..ea093a0
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values-zu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  Copyright 2021 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="appsearch_no_documents_error" msgid="2371906054857961621">"Awekho amadokhumenti we-AppSearch atholakele kusizindalwazi. Qinisekisa ukuthi igama lesizindalwazi livumelekile nokuthi kukhethwe uhlobo lwesitoreji esifanele."</string>
+    <string name="appsearch_no_schema_types_error" msgid="6040453370924007774">"Azikho izinhlobo ze-schema se-AppSearch ezitholakele. Qinisekisa ukuthi igama lesizindalwazi livumelekile nokuthi kukhethwe uhlobo lwesitoreji esifanele."</string>
+    <string name="appsearch_documents_loading" msgid="3079225167662255666">"Ilayisha amadokhumenti we-AppSearch..."</string>
+    <string name="appsearch_schema_types_loading" msgid="5223743742396711620">"Ilayisha izinhlobo ze-schema se-AppSearch..."</string>
+    <string name="appsearch_schema_version" msgid="5289450345246068014">"Uhlobo lwe-Schema: %d"</string>
+</resources>
diff --git a/appsearch/debug-view/src/main/res/values/strings.xml b/appsearch/debug-view/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e6effe7
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values/strings.xml
@@ -0,0 +1,31 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <string name="appsearch_no_documents_error">No AppSearch documents found in database. Verify
+        that the database name is valid and the correct storage type was selected.
+    </string>
+    <string name="appsearch_no_schema_types_error">No AppSearch schema types found. Verify that the
+        database name is valid and the correct storage type was selected.
+    </string>
+
+    <string name="appsearch_documents_loading">Loading AppSearch documents...
+    </string>
+    <string name="appsearch_schema_types_loading">Loading AppSearch schema types...
+    </string>
+
+    <string name="appsearch_schema_version">Schema Version: %d </string>
+</resources>
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index c7b91f5..7bfc923 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -20,6 +20,32 @@
 #
 # Example usage (from root dir of androidx workspace):
 # $ ./frameworks/support/appsearch/exportToFramework.py "$HOME/android/master" "<jetpack changeid>"
+
+# Special directives supported by this script:
+#
+# Causes the file where it appears to not be copied at all:
+#   @exportToFramework:skipFile()
+#
+# Causes the text appearing between startStrip() and endStrip() to be removed during export:
+#   // @exportToFramework:startStrip() ... // @exportToFramework:endStrip()
+#
+# Replaced with @hide:
+#   <!--@exportToFramework:hide-->
+#
+# Replaced with @CurrentTimeMillisLong:
+#   /*@exportToFramework:CurrentTimeMillisLong*/
+#
+# Removes the text appearing between ifJetpack() and else(), and causes the text appearing between
+# else() and --> to become uncommented, to support framework-only Javadocs:
+#   <!--@exportToFramework:ifJetpack()-->
+#   Jetpack-only Javadoc
+#   <!--@exportToFramework:else()
+#   Framework-only Javadoc
+#   -->
+# Note: Using the above pattern, you can hide a method in Jetpack but unhide it in Framework like
+# this:
+#   <!--@exportToFramework:ifJetpack()-->@hide<!--@exportToFramework:else()-->
+
 import os
 import re
 import subprocess
@@ -50,6 +76,7 @@
     def __init__(self, jetpack_appsearch_root, framework_appsearch_root):
         self._jetpack_appsearch_root = jetpack_appsearch_root
         self._framework_appsearch_root = framework_appsearch_root
+        self._written_files = []
 
     def _PruneDir(self, dir_to_prune):
         for walk_path, walk_folders, walk_files in os.walk(dir_to_prune):
@@ -74,18 +101,36 @@
         with open(dest_path, 'w') as fh:
             fh.write(contents)
 
-        # Run formatter
-        google_java_format_cmd = [GOOGLE_JAVA_FORMAT, '--aosp', '-i', dest_path]
-        print('$ ' + ' '.join(google_java_format_cmd))
-        subprocess.check_call(google_java_format_cmd, cwd=self._framework_appsearch_root)
+        # Save file for future formatting
+        self._written_files.append(dest_path)
 
     def _TransformCommonCode(self, contents):
-        # Apply strips
+        # Apply stripping
         contents = re.sub(
-                r'\/\/ @exportToFramework:startStrip\(\).*?\/\/ @exportToFramework:endStrip\(\)',
-                '',
-                contents,
-                flags=re.DOTALL)
+            r'\/\/ @exportToFramework:startStrip\(\).*?\/\/ @exportToFramework:endStrip\(\)',
+            '',
+            contents,
+            flags=re.DOTALL)
+
+        # Apply if/elses in javadocs
+        contents = re.sub(
+            r'<!--@exportToFramework:ifJetpack\(\)-->.*?<!--@exportToFramework:else\(\)(.*?)-->',
+            r'\1',
+            contents,
+            flags=re.DOTALL)
+
+        # Add additional imports if required
+        imports_to_add = []
+        if '@exportToFramework:CurrentTimeMillisLong' in contents:
+            imports_to_add.append('android.annotation.CurrentTimeMillisLong')
+        if '@exportToFramework:UnsupportedAppUsage' in contents:
+            imports_to_add.append('android.compat.annotation.UnsupportedAppUsage')
+        for import_to_add in imports_to_add:
+            contents = re.sub(
+                    r'^(\s*package [^;]+;\s*)$', r'\1\nimport %s;\n' % import_to_add, contents,
+                    flags=re.MULTILINE)
+
+        # Apply in-place replacements
         return (contents
             .replace('androidx.appsearch.app', 'android.app.appsearch')
             .replace(
@@ -104,20 +149,24 @@
             .replace(
                     'androidx.core.util.ObjectsCompat',
                     'java.util.Objects')
+            # Preconditions.checkNotNull is replaced with Objects.requireNonNull. We add both
+            # imports and let google-java-format sort out which one is unused.
             .replace(
-                    'androidx.core.util.Preconditions',
-                    'com.android.internal.util.Preconditions')
+                    'import androidx.core.util.Preconditions;',
+                    'import java.util.Objects; import com.android.internal.util.Preconditions;')
             .replace('import androidx.annotation.RestrictTo;', '')
             .replace('@RestrictTo(RestrictTo.Scope.LIBRARY)', '')
             .replace('@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)', '')
+            .replace('Preconditions.checkNotNull(', 'Objects.requireNonNull(')
             .replace('ObjectsCompat.', 'Objects.')
+            .replace('/*@exportToFramework:CurrentTimeMillisLong*/', '@CurrentTimeMillisLong')
+            .replace('/*@exportToFramework:UnsupportedAppUsage*/', '@UnsupportedAppUsage')
+            .replace('<!--@exportToFramework:hide-->', '@hide')
             .replace('// @exportToFramework:skipFile()', '')
         )
 
     def _TransformTestCode(self, contents):
         contents = (contents
-            .replace('org.junit.Assert.assertThrows', 'org.testng.Assert.expectThrows')
-            .replace('assertThrows(', 'expectThrows(')
             .replace('androidx.appsearch.app.util.', 'com.android.server.appsearch.testing.')
             .replace(
                     'package androidx.appsearch.app.util;',
@@ -155,7 +204,7 @@
         api_test_dest_dir = os.path.join(self._framework_appsearch_root, FRAMEWORK_API_TEST_ROOT)
 
         # CTS tests
-        cts_test_source_dir = os.path.join(api_test_source_dir, 'app/cts')
+        cts_test_source_dir = os.path.join(api_test_source_dir, 'cts')
         cts_test_dest_dir = os.path.join(self._framework_appsearch_root, FRAMEWORK_CTS_TEST_ROOT)
 
         # Test utils
@@ -246,7 +295,7 @@
                     .replace('package androidx.appsearch',
                             'package com.android.server.appsearch.external')
                     .replace('com.google.android.icing.proto.',
-                            'com.android.server.appsearch.proto.')
+                            'com.android.server.appsearch.icing.proto.')
                     .replace('com.google.android.icing.protobuf.',
                             'com.android.server.appsearch.protobuf.')
             )
@@ -254,9 +303,15 @@
         self._TransformAndCopyFolder(
                 impl_test_source_dir, impl_test_dest_dir, transform_func=_TransformImplTestCode)
 
+    def _FormatWrittenFiles(self):
+        google_java_format_cmd = [GOOGLE_JAVA_FORMAT, '--aosp', '-i'] + self._written_files
+        print('$ ' + ' '.join(google_java_format_cmd))
+        subprocess.check_call(google_java_format_cmd, cwd=self._framework_appsearch_root)
+
     def ExportCode(self):
         self._ExportApiCode()
         self._ExportImplCode()
+        self._FormatWrittenFiles()
 
     def WriteChangeIdFile(self, changeid):
         """Copies the changeid of the most recent public CL into a file on the framework side.
diff --git a/appsearch/local-storage/api/current.txt b/appsearch/local-storage/api/current.txt
index e0d39ea..5eb0af5 100644
--- a/appsearch/local-storage/api/current.txt
+++ b/appsearch/local-storage/api/current.txt
@@ -5,22 +5,15 @@
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
-  public static final class LocalStorage.GlobalSearchContext {
-  }
-
-  public static final class LocalStorage.GlobalSearchContext.Builder {
-    ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
-    method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
-  }
-
   public static final class LocalStorage.SearchContext {
     method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
   }
 
   public static final class LocalStorage.SearchContext.Builder {
-    ctor public LocalStorage.SearchContext.Builder(android.content.Context);
+    ctor public LocalStorage.SearchContext.Builder(android.content.Context, String);
     method public androidx.appsearch.localstorage.LocalStorage.SearchContext build();
-    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setDatabaseName(String);
+    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
   }
 
 }
diff --git a/appsearch/local-storage/api/public_plus_experimental_current.txt b/appsearch/local-storage/api/public_plus_experimental_current.txt
index e0d39ea..5eb0af5 100644
--- a/appsearch/local-storage/api/public_plus_experimental_current.txt
+++ b/appsearch/local-storage/api/public_plus_experimental_current.txt
@@ -5,22 +5,15 @@
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
-  public static final class LocalStorage.GlobalSearchContext {
-  }
-
-  public static final class LocalStorage.GlobalSearchContext.Builder {
-    ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
-    method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
-  }
-
   public static final class LocalStorage.SearchContext {
     method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
   }
 
   public static final class LocalStorage.SearchContext.Builder {
-    ctor public LocalStorage.SearchContext.Builder(android.content.Context);
+    ctor public LocalStorage.SearchContext.Builder(android.content.Context, String);
     method public androidx.appsearch.localstorage.LocalStorage.SearchContext build();
-    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setDatabaseName(String);
+    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
   }
 
 }
diff --git a/appsearch/local-storage/api/restricted_current.txt b/appsearch/local-storage/api/restricted_current.txt
index e0d39ea..5eb0af5 100644
--- a/appsearch/local-storage/api/restricted_current.txt
+++ b/appsearch/local-storage/api/restricted_current.txt
@@ -5,22 +5,15 @@
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
-  public static final class LocalStorage.GlobalSearchContext {
-  }
-
-  public static final class LocalStorage.GlobalSearchContext.Builder {
-    ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
-    method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
-  }
-
   public static final class LocalStorage.SearchContext {
     method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
   }
 
   public static final class LocalStorage.SearchContext.Builder {
-    ctor public LocalStorage.SearchContext.Builder(android.content.Context);
+    ctor public LocalStorage.SearchContext.Builder(android.content.Context, String);
     method public androidx.appsearch.localstorage.LocalStorage.SearchContext build();
-    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setDatabaseName(String);
+    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
   }
 
 }
diff --git a/appsearch/local-storage/build.gradle b/appsearch/local-storage/build.gradle
index cb37d65..70cf200 100644
--- a/appsearch/local-storage/build.gradle
+++ b/appsearch/local-storage/build.gradle
@@ -51,6 +51,7 @@
                 targets "icing"
             }
         }
+	multiDexEnabled true
     }
     externalNativeBuild {
         cmake {
@@ -74,10 +75,12 @@
     implementation(project(":appsearch:appsearch"))
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
     implementation("androidx.core:core:1.2.0")
+    implementation "androidx.multidex:multidex:2.0.1"
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.mockitoAndroid)
     //TODO(b/149787478) upgrade and link to Dependencies.kt
     androidTestImplementation("junit:junit:4.13")
 }
diff --git a/appsearch/local-storage/proguard-rules.pro b/appsearch/local-storage/proguard-rules.pro
index b18f891..82c4b719 100644
--- a/appsearch/local-storage/proguard-rules.pro
+++ b/appsearch/local-storage/proguard-rules.pro
@@ -18,3 +18,13 @@
 -keepclassmembers class * extends com.google.android.icing.protobuf.GeneratedMessageLite {
   <fields>;
 }
+-keep class com.google.android.icing.BreakIteratorBatcher { *; }
+-keepclassmembers public class com.google.android.icing.IcingSearchEngine {
+  private long nativePointer;
+}
+
+# This prevents the names of native methods from being obfuscated and prevents
+# UnsatisfiedLinkErrors.
+-keepclasseswithmembernames,includedescriptorclasses class * {
+  native <methods>;
+}
diff --git a/appsearch/local-storage/src/androidTest/AndroidManifest.xml b/appsearch/local-storage/src/androidTest/AndroidManifest.xml
index 7638c0f..014f4a2 100644
--- a/appsearch/local-storage/src/androidTest/AndroidManifest.xml
+++ b/appsearch/local-storage/src/androidTest/AndroidManifest.xml
@@ -16,4 +16,6 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="androidx.appsearch.localstorage.test">
+    <!-- Required for junit TemporaryFolder rule on older API levels -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 </manifest>
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 4ce9593..eb4557b 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -16,30 +16,50 @@
 
 package androidx.appsearch.localstorage;
 
+import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
+import android.content.Context;
+import android.os.Process;
+
+import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
-import androidx.appsearch.localstorage.converter.SchemaToProtoConverter;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.OptimizeStats;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
+import com.google.android.icing.proto.PersistType;
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.PutResultProto;
 import com.google.android.icing.proto.SchemaProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SearchResultProto;
 import com.google.android.icing.proto.SearchSpecProto;
+import com.google.android.icing.proto.StatusProto;
+import com.google.android.icing.proto.StorageInfoProto;
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 import org.junit.Before;
@@ -47,37 +67,30 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 public class AppSearchImplTest {
+    /**
+     * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
+     */
+    private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
     @Rule
     public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
     private AppSearchImpl mAppSearchImpl;
-    private SchemaTypeConfigProto mVisibilitySchemaProto;
 
     @Before
     public void setUp() throws Exception {
-        mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder());
-
-        AppSearchSchema visibilitySchema = VisibilityStore.SCHEMA;
-
-        // We need to rewrite the schema type to follow AppSearchImpl's prefixing scheme.
-        AppSearchSchema.Builder rewrittenVisibilitySchema =
-                new AppSearchSchema.Builder(AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME) + VisibilityStore.SCHEMA_TYPE);
-        List<AppSearchSchema.PropertyConfig> visibilityProperties =
-                visibilitySchema.getProperties();
-        for (AppSearchSchema.PropertyConfig property : visibilityProperties) {
-            rewrittenVisibilitySchema.addProperty(property);
-        }
-        mVisibilitySchemaProto =
-                SchemaToProtoConverter.toSchemaTypeConfigProto(rewrittenVisibilitySchema.build());
+        mAppSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
     }
 
-    //TODO(b/175430168) add test to verify reset is working properly.
-
     /**
      * Ensure that we can rewrite an incoming schema type by adding the database as a prefix. While
      * also keeping any other existing schema types that may already be part of Icing's persisted
@@ -92,38 +105,52 @@
         // Create a copy so we can modify it.
         List<SchemaTypeConfigProto> existingTypes =
                 new ArrayList<>(existingSchemaBuilder.getTypesList());
-
-        SchemaProto newSchema = SchemaProto.newBuilder()
-                .addTypes(SchemaTypeConfigProto.newBuilder()
-                        .setSchemaType("Foo").build())
-                .addTypes(SchemaTypeConfigProto.newBuilder()
-                        .setSchemaType("TestType")
-                        .addProperties(PropertyConfigProto.newBuilder()
-                                .setPropertyName("subject")
-                                .setDataType(PropertyConfigProto.DataType.Code.STRING)
-                                .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
-                                .setStringIndexingConfig(StringIndexingConfig.newBuilder()
-                                        .setTokenizerType(
-                                                StringIndexingConfig.TokenizerType.Code.PLAIN)
-                                        .setTermMatchType(TermMatchType.Code.PREFIX)
-                                        .build()
-                                ).build()
-                        ).addProperties(PropertyConfigProto.newBuilder()
-                                .setPropertyName("link")
-                                .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
-                                .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
-                                .setSchemaType("RefType")
+        SchemaTypeConfigProto schemaTypeConfigProto1 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType("Foo").build();
+        SchemaTypeConfigProto schemaTypeConfigProto2 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType("TestType")
+                .addProperties(PropertyConfigProto.newBuilder()
+                        .setPropertyName("subject")
+                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                        .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                        .setStringIndexingConfig(StringIndexingConfig.newBuilder()
+                                .setTokenizerType(
+                                        StringIndexingConfig.TokenizerType.Code.PLAIN)
+                                .setTermMatchType(TermMatchType.Code.PREFIX)
                                 .build()
                         ).build()
+                ).addProperties(PropertyConfigProto.newBuilder()
+                        .setPropertyName("link")
+                        .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+                        .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                        .setSchemaType("RefType")
+                        .build()
                 ).build();
+        SchemaTypeConfigProto schemaTypeConfigProto3 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType("RefType").build();
+        SchemaProto newSchema = SchemaProto.newBuilder()
+                .addTypes(schemaTypeConfigProto1)
+                .addTypes(schemaTypeConfigProto2)
+                .addTypes(schemaTypeConfigProto3)
+                .build();
 
-        AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema(
-                AppSearchImpl.createPrefix("package", "newDatabase"), existingSchemaBuilder,
+        AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = AppSearchImpl.rewriteSchema(
+                createPrefix("package", "newDatabase"), existingSchemaBuilder,
                 newSchema);
 
         // We rewrote all the new types that were added. And nothing was removed.
-        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes)
-                .containsExactly("package$newDatabase/Foo", "package$newDatabase/TestType");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet()).containsExactly(
+                "package$newDatabase/Foo", "package$newDatabase/TestType",
+                "package$newDatabase/RefType");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.get(
+                "package$newDatabase/Foo").getSchemaType()).isEqualTo(
+                "package$newDatabase/Foo");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.get(
+                "package$newDatabase/TestType").getSchemaType()).isEqualTo(
+                "package$newDatabase/TestType");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.get(
+                "package$newDatabase/RefType").getSchemaType()).isEqualTo(
+                "package$newDatabase/RefType");
         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes).isEmpty();
 
         SchemaProto expectedSchema = SchemaProto.newBuilder()
@@ -148,6 +175,8 @@
                                 .setSchemaType("package$newDatabase/RefType")
                                 .build()
                         ).build())
+                .addTypes(SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$newDatabase/RefType").build())
                 .build();
 
         existingTypes.addAll(expectedSchema.getTypesList());
@@ -169,13 +198,13 @@
                         .setSchemaType("Foo").build())
                 .build();
 
-        AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema(
-                AppSearchImpl.createPrefix("package", "existingDatabase"), existingSchemaBuilder,
+        AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = AppSearchImpl.rewriteSchema(
+                createPrefix("package", "existingDatabase"), existingSchemaBuilder,
                 newSchema);
 
         // Nothing was removed, but the method did rewrite the type name.
-        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes)
-                .containsExactly("package$existingDatabase/Foo");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet()).containsExactly(
+                "package$existingDatabase/Foo");
         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes).isEmpty();
 
         // Same schema since nothing was added.
@@ -199,14 +228,15 @@
                         .setSchemaType("Bar").build())
                 .build();
 
-        AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema(
-                AppSearchImpl.createPrefix("package", "existingDatabase"), existingSchemaBuilder,
+        AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = AppSearchImpl.rewriteSchema(
+                createPrefix("package", "existingDatabase"), existingSchemaBuilder,
                 newSchema);
 
         // Bar type was rewritten, but Foo ended up being deleted since it wasn't included in the
         // new schema.
         assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes)
-                .containsExactly("package$existingDatabase/Bar");
+                .containsKey("package$existingDatabase/Bar");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet().size()).isEqualTo(1);
         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes)
                 .containsExactly("package$existingDatabase/Foo");
 
@@ -223,31 +253,31 @@
     @Test
     public void testAddDocumentTypePrefix() {
         DocumentProto insideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
                 .build();
 
         DocumentProto expectedInsideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .build();
         DocumentProto expectedDocumentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(expectedInsideDocument))
                 .build();
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
-        mAppSearchImpl.addPrefixToDocument(actualDocument, AppSearchImpl.createPrefix("package",
+        addPrefixToDocument(actualDocument, createPrefix("package",
                 "databaseName"));
         assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
     }
@@ -255,62 +285,62 @@
     @Test
     public void testRemoveDocumentTypePrefixes() throws Exception {
         DocumentProto insideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
                 .build();
 
         DocumentProto expectedInsideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .build();
 
         DocumentProto expectedDocumentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(expectedInsideDocument))
                 .build();
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
-        assertThat(mAppSearchImpl.removePrefixesFromDocument(actualDocument)).isEqualTo(
+        assertThat(removePrefixesFromDocument(actualDocument)).isEqualTo(
                 "package$databaseName/");
         assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
     }
 
     @Test
-    public void testRemoveDatabasesFromDocumentThrowsException() throws Exception {
+    public void testRemoveDatabasesFromDocumentThrowsException() {
         // Set two different database names in the document, which should never happen
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("prefix1/type")
                 .setNamespace("prefix2/namespace")
                 .build();
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
         AppSearchException e = assertThrows(AppSearchException.class, () ->
-                mAppSearchImpl.removePrefixesFromDocument(actualDocument));
+                removePrefixesFromDocument(actualDocument));
         assertThat(e).hasMessageThat().contains("Found unexpected multiple prefix names");
     }
 
     @Test
-    public void testNestedRemoveDatabasesFromDocumentThrowsException() throws Exception {
+    public void testNestedRemoveDatabasesFromDocumentThrowsException() {
         // Set two different database names in the outer and inner document, which should never
         // happen.
         DocumentProto insideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("prefix1/type")
                 .setNamespace("prefix1/namespace")
                 .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("prefix2/type")
                 .setNamespace("prefix2/namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
@@ -318,53 +348,188 @@
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
         AppSearchException e = assertThrows(AppSearchException.class, () ->
-                mAppSearchImpl.removePrefixesFromDocument(actualDocument));
+                removePrefixesFromDocument(actualDocument));
         assertThat(e).hasMessageThat().contains("Found unexpected multiple prefix names");
     }
 
     @Test
-    public void testOptimize() throws Exception {
+    public void testTriggerCheckOptimizeByMutationSize() throws Exception {
         // Insert schema
         List<AppSearchSchema> schemas =
                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
-        mAppSearchImpl.setSchema("package", "database", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
-        // Insert enough documents.
-        for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
-                + AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
-            GenericDocument document =
-                    new GenericDocument.Builder<>("uri" + i, "type").setNamespace(
-                            "namespace").build();
-            mAppSearchImpl.putDocument("package", "database", document);
-        }
+        // Insert a document and then remove it to generate garbage.
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
+        mAppSearchImpl.remove("package", "database", "namespace", "id",
+                /*removeStatsBuilder=*/ null);
 
-        // Check optimize() will release 0 docs since there is no deletion.
+        // Verify there is garbage documents.
         GetOptimizeInfoResultProto optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
+        assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(1);
+
+        // Increase mutation counter and stop before reach the threshold
+        mAppSearchImpl.checkForOptimize(AppSearchImpl.CHECK_OPTIMIZE_INTERVAL - 1,
+                /*builder=*/null);
+
+        // Verify the optimize() isn't triggered.
+        optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
+        assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(1);
+
+        // Increase the counter and reach the threshold, optimize() should be triggered.
+        OptimizeStats.Builder builder = new OptimizeStats.Builder();
+        mAppSearchImpl.checkForOptimize(/*mutateBatchSize=*/ 1, builder);
+
+        // Verify optimize() is triggered.
+        optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
         assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(0);
+        assertThat(optimizeInfo.getEstimatedOptimizableBytes()).isEqualTo(0);
 
-        // delete 999 documents , we will reach the threshold to trigger optimize() in next
-        // deletion.
-        for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT - 1; i++) {
-            mAppSearchImpl.remove("package", "database", "namespace", "uri" + i);
-        }
+        // Verify the stats have been set.
+        OptimizeStats oStats = builder.build();
+        assertThat(oStats.getOriginalDocumentCount()).isEqualTo(1);
+        assertThat(oStats.getDeletedDocumentCount()).isEqualTo(1);
+    }
 
-        // optimize() still not be triggered since we are in the interval to call getOptimizeInfo()
-        optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
-        assertThat(optimizeInfo.getOptimizableDocs())
-                .isEqualTo(AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT - 1);
+    @Test
+    public void testReset() throws Exception {
+        // Setup the index
+        Context context = ApplicationProvider.getApplicationContext();
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
 
-        // Keep delete docs, will reach the interval this time and trigger optimize().
-        for (int i = AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT;
-                i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
-                        + AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
-            mAppSearchImpl.remove("package", "database", "namespace", "uri" + i);
-        }
+        // Insert schema
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("Type1").build(),
+                new AppSearchSchema.Builder("Type2").build());
+        appSearchImpl.setSchema(
+                context.getPackageName(),
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
-        // Verify optimize() is triggered
-        optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
-        assertThat(optimizeInfo.getOptimizableDocs())
-                .isLessThan(AppSearchImpl.CHECK_OPTIMIZE_INTERVAL);
+        // Insert a valid doc
+        GenericDocument validDoc =
+                new GenericDocument.Builder<>("namespace1", "id1", "Type1").build();
+        appSearchImpl.putDocument(
+                context.getPackageName(),
+                "database1",
+                validDoc,
+                /*logger=*/null);
+
+        // Query it via global query. We use the same code again later so this is to make sure we
+        // have our global query configured right.
+        SearchResultPage results = appSearchImpl.globalQuery(
+                /*queryExpression=*/ "",
+                new SearchSpec.Builder().addFilterSchemas("Type1").build(),
+                context.getPackageName(),
+                /*visibilityStore=*/ null,
+                Process.INVALID_UID,
+                /*callerHasSystemAccess=*/ false,
+                /*logger=*/ null);
+        assertThat(results.getResults()).hasSize(1);
+        assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
+
+        // Create a doc with a malformed namespace
+        DocumentProto invalidDoc = DocumentProto.newBuilder()
+                .setNamespace("invalidNamespace")
+                .setUri("id2")
+                .setSchema(context.getPackageName() + "$database1/Type1")
+                .build();
+        AppSearchException e = assertThrows(
+                AppSearchException.class,
+                () -> PrefixUtil.getPrefix(invalidDoc.getNamespace()));
+        assertThat(e).hasMessageThat().isEqualTo(
+                "The prefixed value \"invalidNamespace\" doesn't contain a valid database name");
+
+        // Insert the invalid doc with an invalid namespace right into icing
+        PutResultProto putResultProto = appSearchImpl.mIcingSearchEngineLocked.put(invalidDoc);
+        assertThat(putResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK);
+
+        // Initialize AppSearchImpl. This should cause a reset.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
+        appSearchImpl.close();
+        appSearchImpl = AppSearchImpl.create(
+                appsearchDir, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+
+        // Check recovery state
+        InitializeStats initStats = initStatsBuilder.build();
+        assertThat(initStats).isNotNull();
+        assertThat(initStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_INTERNAL_ERROR);
+        assertThat(initStats.hasDeSync()).isFalse();
+        assertThat(initStats.getDocumentStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+        assertThat(initStats.getIndexRestorationCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+        assertThat(initStats.getSchemaStoreRecoveryCause())
+                .isEqualTo(InitializeStats.RECOVERY_CAUSE_NONE);
+        assertThat(initStats.getDocumentStoreDataStatus())
+                .isEqualTo(InitializeStats.DOCUMENT_STORE_DATA_STATUS_NO_DATA_LOSS);
+        assertThat(initStats.hasReset()).isTrue();
+        assertThat(initStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+
+        // Make sure all our data is gone
+        assertThat(appSearchImpl.getSchema(context.getPackageName(), "database1").getSchemas())
+                .isEmpty();
+        results = appSearchImpl.globalQuery(
+                /*queryExpression=*/ "",
+                new SearchSpec.Builder().addFilterSchemas("Type1").build(),
+                context.getPackageName(),
+                /*visibilityStore=*/ null,
+                Process.INVALID_UID,
+                /*callerHasSystemAccess=*/ false,
+                /*logger=*/ null);
+        assertThat(results.getResults()).isEmpty();
+
+        // Make sure the index can now be used successfully
+        appSearchImpl.setSchema(
+                context.getPackageName(),
+                "database1",
+                Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert a valid doc
+        appSearchImpl.putDocument(
+                context.getPackageName(),
+                "database1",
+                validDoc,
+                /*logger=*/null);
+
+        // Query it via global query.
+        results = appSearchImpl.globalQuery(
+                /*queryExpression=*/ "",
+                new SearchSpec.Builder().addFilterSchemas("Type1").build(),
+                context.getPackageName(),
+                /*visibilityStore=*/ null,
+                Process.INVALID_UID,
+                /*callerHasSystemAccess=*/ false,
+                /*logger=*/ null);
+        assertThat(results.getResults()).hasSize(1);
+        assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
     }
 
     @Test
@@ -375,17 +540,26 @@
         // Insert schema
         List<AppSearchSchema> schemas =
                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
-        mAppSearchImpl.setSchema("package", "database", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
         // Insert document
-        GenericDocument document = new GenericDocument.Builder<>("uri", "type").setNamespace(
-                "namespace").build();
-        mAppSearchImpl.putDocument("package", "database", document);
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
 
         // Rewrite SearchSpec
         mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
-                Collections.singleton(AppSearchImpl.createPrefix("package", "database")));
+                Collections.singleton(createPrefix("package", "database")),
+                ImmutableSet.of("package$database/type"));
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
                 "package$database/type");
         assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
@@ -401,24 +575,42 @@
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("typeA").build(),
                 new AppSearchSchema.Builder("typeB").build());
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        mAppSearchImpl.setSchema("package", "database2", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database2",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
         // Insert documents
-        GenericDocument document1 = new GenericDocument.Builder<>("uri", "typeA").setNamespace(
-                "namespace").build();
-        mAppSearchImpl.putDocument("package", "database1", document1);
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id",
+                "typeA").build();
+        mAppSearchImpl.putDocument("package", "database1", document1, /*logger=*/ null);
 
-        GenericDocument document2 = new GenericDocument.Builder<>("uri", "typeB").setNamespace(
-                "namespace").build();
-        mAppSearchImpl.putDocument("package", "database2", document2);
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id",
+                "typeB").build();
+        mAppSearchImpl.putDocument("package", "database2", document2, /*logger=*/ null);
 
         // Rewrite SearchSpec
         mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
-                ImmutableSet.of(AppSearchImpl.createPrefix("package", "database1"),
-                        AppSearchImpl.createPrefix("package", "database2")));
+                ImmutableSet.of(createPrefix("package", "database1"),
+                        createPrefix("package", "database2")), ImmutableSet.of(
+                        "package$database1/typeA", "package$database1/typeB",
+                        "package$database2/typeA", "package$database2/typeB"));
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
                 "package$database1/typeA", "package$database1/typeB", "package$database2/typeA",
                 "package$database2/typeB");
@@ -427,138 +619,750 @@
     }
 
     @Test
+    public void testRewriteSearchSpec_ignoresSearchSpecSchemaFilters() throws Exception {
+        SearchSpecProto.Builder searchSpecProto =
+                SearchSpecProto.newBuilder().setQuery("").addSchemaTypeFilters("type");
+
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
+
+        // If 'allowedPrefixedSchemas' is empty, this returns false since there's nothing to
+        // search over. Despite the searchSpecProto having schema type filters.
+        assertThat(mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
+                Collections.singleton(createPrefix("package", "database")),
+                /*allowedPrefixedSchemas=*/ Collections.emptySet())).isFalse();
+    }
+
+    @Test
     public void testQueryEmptyDatabase() throws Exception {
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
         SearchResultPage searchResultPage = mAppSearchImpl.query("package", "EmptyDatabase", "",
-                searchSpec);
+                searchSpec, /*logger=*/ null);
         assertThat(searchResultPage.getResults()).isEmpty();
     }
 
+    /**
+     * TODO(b/169883602): This should be an integration test at the cts-level. This is a
+     * short-term test until we have official support for multiple-apps indexing at once.
+     */
+    @Test
+    public void testQueryWithMultiplePackages_noPackageFilters() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert package2 schema
+        List<AppSearchSchema> schema2 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
+        mAppSearchImpl.setSchema(
+                "package2",
+                "database2",
+                schema2,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert package1 document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id", "schema1")
+                .build();
+        mAppSearchImpl.putDocument("package1", "database1", document, /*logger=*/ null);
+
+        // No query filters specified, package2 shouldn't be able to query for package1's documents.
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package2", "database2", "",
+                searchSpec, /*logger=*/ null);
+        assertThat(searchResultPage.getResults()).isEmpty();
+
+        // Insert package2 document
+        document = new GenericDocument.Builder<>("namespace", "id", "schema2").build();
+        mAppSearchImpl.putDocument("package2", "database2", document, /*logger=*/ null);
+
+        // No query filters specified. package2 should only get its own documents back.
+        searchResultPage = mAppSearchImpl.query("package2", "database2", "", searchSpec, /*logger=
+         */ null);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
+    }
+
+    /**
+     * TODO(b/169883602): This should be an integration test at the cts-level. This is a
+     * short-term test until we have official support for multiple-apps indexing at once.
+     */
+    @Test
+    public void testQueryWithMultiplePackages_withPackageFilters() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert package2 schema
+        List<AppSearchSchema> schema2 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
+        mAppSearchImpl.setSchema(
+                "package2",
+                "database2",
+                schema2,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert package1 document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document, /*logger=*/ null);
+
+        // "package1" filter specified, but package2 shouldn't be able to query for package1's
+        // documents.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .addFilterPackageNames("package1")
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package2", "database2", "",
+                searchSpec, /*logger=*/ null);
+        assertThat(searchResultPage.getResults()).isEmpty();
+
+        // Insert package2 document
+        document = new GenericDocument.Builder<>("namespace", "id", "schema2").build();
+        mAppSearchImpl.putDocument("package2", "database2", document, /*logger=*/ null);
+
+        // "package2" filter specified, package2 should only get its own documents back.
+        searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .addFilterPackageNames("package2")
+                .build();
+        searchResultPage = mAppSearchImpl.query("package2", "database2", "", searchSpec, /*logger=
+         */ null);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
+    }
+
     @Test
     public void testGlobalQueryEmptyDatabase() throws Exception {
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
-        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery("", searchSpec);
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+                "",
+                searchSpec,
+                /*callerPackageName=*/ "",
+                /*visibilityStore=*/ null,
+                Process.INVALID_UID,
+                /*callerHasSystemAccess=*/ false,
+                /*logger=*/ null);
         assertThat(searchResultPage.getResults()).isEmpty();
     }
 
     @Test
+    public void testGetNextPageToken_query() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package1", "database1", "",
+                searchSpec, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+        searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
+    }
+
+    @Test
+    public void testGetNextPageWithDifferentPackage_query() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package1", "database1", "",
+                searchSpec, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+
+        // Try getting next page with the wrong package, package2
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getNextPage("package2",
+                        nextPageToken));
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package2\" cannot use nextPageToken: " + nextPageToken);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+
+        // Can continue getting next page for package1
+        searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
+    }
+
+    @Test
+    public void testGetNextPageToken_globalQuery() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
+                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
+                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+        searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
+    }
+
+    @Test
+    public void testGetNextPageWithDifferentPackage_globalQuery() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
+                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
+                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+
+        // Try getting next page with the wrong package, package2
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getNextPage("package2", nextPageToken));
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package2\" cannot use nextPageToken: " + nextPageToken);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+
+        // Can continue getting next page for package1
+        searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
+    }
+
+    @Test
+    public void testInvalidateNextPageToken_query() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package1", "database1", "",
+                searchSpec, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+
+        // Invalidate the token
+        mAppSearchImpl.invalidateNextPageToken("package1", nextPageToken);
+
+        // Can't get next page because we invalidated the token.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getNextPage("package1", nextPageToken));
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package1\" cannot use nextPageToken: " + nextPageToken);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+    }
+
+    @Test
+    public void testInvalidateNextPageTokenWithDifferentPackage_query() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package1", "database1", "",
+                searchSpec, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+
+        // Try getting next page with the wrong package, package2
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.invalidateNextPageToken("package2",
+                        nextPageToken));
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package2\" cannot use nextPageToken: " + nextPageToken);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+
+        // Can continue getting next page for package1
+        searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
+    }
+
+    @Test
+    public void testInvalidateNextPageToken_globalQuery() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
+                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
+                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+
+        // Invalidate the token
+        mAppSearchImpl.invalidateNextPageToken("package1", nextPageToken);
+
+        // Can't get next page because we invalidated the token.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getNextPage("package1", nextPageToken));
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package1\" cannot use nextPageToken: " + nextPageToken);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+    }
+
+    @Test
+    public void testInvalidateNextPageTokenWithDifferentPackage_globalQuery() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schema1,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package1", "database1", document2, /*logger=*/ null);
+
+        // Query for only 1 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .setResultCountPerPage(1)
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
+                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
+                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+
+        // Document2 will come first because it was inserted last and default return order is
+        // most recent.
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+
+        long nextPageToken = searchResultPage.getNextPageToken();
+
+        // Try getting next page with the wrong package, package2
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.invalidateNextPageToken("package2",
+                        nextPageToken));
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package2\" cannot use nextPageToken: " + nextPageToken);
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+
+        // Can continue getting next page for package1
+        searchResultPage = mAppSearchImpl.getNextPage("package1", nextPageToken);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
+    }
+
+    @Test
     public void testRemoveEmptyDatabase_noExceptionThrown() throws Exception {
         SearchSpec searchSpec =
-                new SearchSpec.Builder().addSchemaType("FakeType").setTermMatch(
+                new SearchSpec.Builder().addFilterSchemas("FakeType").setTermMatch(
                         TermMatchType.Code.PREFIX_VALUE).build();
         mAppSearchImpl.removeByQuery("package", "EmptyDatabase",
-                "", searchSpec);
+                "", searchSpec, /*statsBuilder=*/ null);
 
         searchSpec =
-                new SearchSpec.Builder().addNamespace("FakeNamespace").setTermMatch(
+                new SearchSpec.Builder().addFilterNamespaces("FakeNamespace").setTermMatch(
                         TermMatchType.Code.PREFIX_VALUE).build();
         mAppSearchImpl.removeByQuery("package", "EmptyDatabase",
-                "", searchSpec);
+                "", searchSpec, /*statsBuilder=*/ null);
 
         searchSpec = new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
-        mAppSearchImpl.removeByQuery("package", "EmptyDatabase", "", searchSpec);
+        mAppSearchImpl.removeByQuery("package", "EmptyDatabase", "", searchSpec,
+                /*statsBuilder=*/ null);
     }
 
     @Test
     public void testSetSchema() throws Exception {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+
         List<AppSearchSchema> schemas =
                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
         // Set schema Email to AppSearch database1
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .build();
 
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
     }
 
     @Test
-    public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singletonList("schema1"), /*forceOverride=*/ false);
-
-        // "schema1" is platform hidden now
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema1")).isFalse();
-
-        // Add a new schema, and include the already-existing "schema1"
+    public void testSetSchema_incompatible() throws Exception {
+        List<AppSearchSchema> oldSchemas = new ArrayList<>();
+        oldSchemas.add(new AppSearchSchema.Builder("Email")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("foo")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .build());
+        oldSchemas.add(new AppSearchSchema.Builder("Text").build());
+        // Set schema Email to AppSearch database1
         mAppSearchImpl.setSchema(
-                "package", "database",
-                ImmutableList.of(
-                        new AppSearchSchema.Builder("schema1").build(),
-                        new AppSearchSchema.Builder("schema2").build()),
-                /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"),
-                /*forceOverride=*/ false);
+                "package",
+                "database1",
+                oldSchemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
-        // Check that "schema1" is still platform hidden, but "schema2" is the default platform
-        // visible.
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema1")).isFalse();
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema2")).isTrue();
+        // Create incompatible schema
+        List<AppSearchSchema> newSchemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Email").build());
+
+        // set email incompatible and delete text
+        SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                newSchemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ true,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Text");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("Email");
     }
 
     @Test
     public void testRemoveSchema() throws Exception {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Email").build(),
                 new AppSearchSchema.Builder("Document").build());
         // Set schema Email and Document to AppSearch database1
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document"))
+                        "package$database1/Document").setVersion(0))
                 .build();
 
         // Check both schema Email and Document saved correctly.
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
 
         final List<AppSearchSchema> finalSchemas = Collections.singletonList(
-                new AppSearchSchema.Builder(
-                        "Email").build());
-        // Check the incompatible error has been thrown.
-        AppSearchException e = assertThrows(AppSearchException.class, () ->
-                mAppSearchImpl.setSchema("package", "database1",
-                        finalSchemas, /*schemasNotPlatformSurfaceable=*/
-                        Collections.emptyList(), /*forceOverride=*/ false));
-        assertThat(e).hasMessageThat().contains("Schema is incompatible");
-        assertThat(e).hasMessageThat().contains("Deleted types: [package$database1/Document]");
+                new AppSearchSchema.Builder("Email").build());
+        SetSchemaResponse setSchemaResponse =
+                mAppSearchImpl.setSchema(
+                        "package",
+                        "database1",
+                        finalSchemas,
+                        /*visibilityStore=*/ null,
+                        /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                        /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                        /*forceOverride=*/ false,
+                        /*version=*/ 0,
+                        /* setSchemaStatsBuilder= */ null);
+        // Check the Document type has been deleted.
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Document");
 
         // ForceOverride to delete.
-        mAppSearchImpl.setSchema("package", "database1",
-                finalSchemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ true);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                finalSchemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ true,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
         // Check Document schema is removed.
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .build();
 
         expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
@@ -566,173 +1370,261 @@
 
     @Test
     public void testRemoveSchema_differentDataBase() throws Exception {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+
         // Create schemas
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Email").build(),
                 new AppSearchSchema.Builder("Document").build());
 
         // Set schema Email and Document to AppSearch database1 and 2
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        mAppSearchImpl.setSchema("package", "database2", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database2",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document"))
+                        "package$database1/Document").setVersion(0))
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database2/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document"))
+                        "package$database2/Document").setVersion(0))
                 .build();
 
         // Check Email and Document is saved in database 1 and 2 correctly.
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
 
         // Save only Email to database1 this time.
         schemas = Collections.singletonList(new AppSearchSchema.Builder("Email").build());
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ true);
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ true,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
         // Create expected schemaType list, database 1 should only contain Email but database 2
         // remains in same.
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database2/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document"))
+                        "package$database2/Document").setVersion(0))
                 .build();
 
         // Check nothing changed in database2.
         expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
     }
 
-
     @Test
-    public void testRemoveSchema_removedFromVisibilityStore() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singletonList("schema1"), /*forceOverride=*/ false);
+    public void testClearPackageData() throws AppSearchException {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+        Map<String, Set<String>> existingDatabases = mAppSearchImpl.getPackageToDatabases();
 
-        // "schema1" is platform hidden now
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema1")).isFalse();
+        // Insert package schema
+        List<AppSearchSchema> schema =
+                ImmutableList.of(new AppSearchSchema.Builder("schema").build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schema,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
 
-        // Remove "schema1" by force overriding
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.emptyList(), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ true);
+        // Insert package document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "schema").build();
+        mAppSearchImpl.putDocument("package", "database", document,
+                /*logger=*/ null);
 
-        // Check that "schema1" is no longer considered platform hidden
-        assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                        prefix, prefix + "schema1")).isTrue();
+        // Verify the document is indexed.
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package",
+                "database",  /*queryExpression=*/ "", searchSpec, /*logger=*/ null);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
 
-        // Add "schema1" back, it gets default visibility settings which means it's not platform
-        // hidden.
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                        prefix, prefix + "schema1")).isTrue();
+        // Remove the package
+        mAppSearchImpl.clearPackageData("package");
+
+        // Verify the document is cleared.
+        searchResultPage = mAppSearchImpl.query("package2", "database2",
+                /*queryExpression=*/ "", searchSpec, /*logger=*/ null);
+        assertThat(searchResultPage.getResults()).isEmpty();
+
+        // Verify the schema is cleared.
+        assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
+                .containsExactlyElementsIn(existingSchemas);
+        assertThat(mAppSearchImpl.getPackageToDatabases())
+                .containsExactlyEntriesIn(existingDatabases);
     }
 
     @Test
-    public void testSetSchema_defaultPlatformVisible() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                        prefix, prefix + "Schema")).isTrue();
+    public void testPrunePackageData() throws AppSearchException {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+        Map<String, Set<String>> existingDatabases = mAppSearchImpl.getPackageToDatabases();
+
+        Set<String> existingPackages = new ArraySet<>(existingSchemas.size());
+        for (int i = 0; i < existingSchemas.size(); i++) {
+            existingPackages.add(PrefixUtil.getPackageName(existingSchemas.get(i).getSchemaType()));
+        }
+
+        // Insert schema for package A and B.
+        List<AppSearchSchema> schema =
+                ImmutableList.of(new AppSearchSchema.Builder("schema").build());
+        mAppSearchImpl.setSchema(
+                "packageA",
+                "database",
+                schema,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "packageB",
+                "database",
+                schema,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Verify these two packages is stored in AppSearch
+        SchemaProto expectedProto = SchemaProto.newBuilder()
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("packageA$database/schema").setVersion(0))
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("packageB$database/schema").setVersion(0))
+                .build();
+        List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
+        expectedTypes.addAll(existingSchemas);
+        expectedTypes.addAll(expectedProto.getTypesList());
+        assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
+                .containsExactlyElementsIn(expectedTypes);
+
+        // Prune packages
+        mAppSearchImpl.prunePackageData(existingPackages);
+
+        // Verify the schema is same as beginning.
+        assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
+                .containsExactlyElementsIn(existingSchemas);
+        assertThat(mAppSearchImpl.getPackageToDatabases())
+                .containsExactlyEntriesIn(existingDatabases);
     }
 
     @Test
-    public void testSetSchema_platformHidden() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singletonList("Schema"), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "Schema")).isFalse();
-    }
-
-    @Test
-    public void testHasSchemaType() throws Exception {
-        // Nothing exists yet
-        assertThat(mAppSearchImpl.hasSchemaTypeLocked("package", "database", "Schema")).isFalse();
-
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.hasSchemaTypeLocked("package", "database", "Schema")).isTrue();
-
-        assertThat(mAppSearchImpl.hasSchemaTypeLocked("package", "database",
-                "UnknownSchema")).isFalse();
-    }
-
-    @Test
-    public void testGetDatabases() throws Exception {
-        // No client databases exist yet, but the VisibilityStore's does
-        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactly(
-                AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME));
+    public void testGetPackageToDatabases() throws Exception {
+        Map<String, Set<String>> existingMapping = mAppSearchImpl.getPackageToDatabases();
+        Map<String, Set<String>> expectedMapping = new ArrayMap<>();
+        expectedMapping.putAll(existingMapping);
 
         // Has database1
-        mAppSearchImpl.setSchema("package", "database1",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactly(
-                AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME),
-                AppSearchImpl.createPrefix("package", "database1"));
+        expectedMapping.put("package1", ImmutableSet.of("database1"));
+        mAppSearchImpl.setSchema(
+                "package1", "database1",
+                Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(mAppSearchImpl.getPackageToDatabases()).containsExactlyEntriesIn(
+                expectedMapping);
 
         // Has both databases
-        mAppSearchImpl.setSchema("package", "database2",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactly(
-                AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME),
-                AppSearchImpl.createPrefix("package", "database1"), AppSearchImpl.createPrefix(
-                        "package", "database2"));
+        expectedMapping.put("package1", ImmutableSet.of("database1", "database2"));
+        mAppSearchImpl.setSchema(
+                "package1", "database2",
+                Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(mAppSearchImpl.getPackageToDatabases()).containsExactlyEntriesIn(
+                expectedMapping);
+
+        // Has both packages
+        expectedMapping.put("package2", ImmutableSet.of("database1"));
+        mAppSearchImpl.setSchema(
+                "package2", "database1",
+                Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(mAppSearchImpl.getPackageToDatabases()).containsExactlyEntriesIn(
+                expectedMapping);
     }
 
     @Test
     public void testRewriteSearchResultProto() throws Exception {
-        final String database =
-                "com.package.foo" + AppSearchImpl.PACKAGE_DELIMITER + "databaseName"
-                        + AppSearchImpl.DATABASE_DELIMITER;
-        final String uri = "uri";
-        final String namespace = database + "namespace";
-        final String schemaType = database + "schema";
+        final String prefix =
+                "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
+                        + PrefixUtil.DATABASE_DELIMITER;
+        final String id = "id";
+        final String namespace = prefix + "namespace";
+        final String schemaType = prefix + "schema";
 
         // Building the SearchResult received from query.
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri(uri)
+                .setUri(id)
                 .setNamespace(namespace)
                 .setSchema(schemaType)
                 .build();
@@ -742,16 +1634,1270 @@
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
                 .addResults(resultProto)
                 .build();
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType(schemaType)
+                        .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
+                ImmutableMap.of(schemaType, schemaTypeConfigProto));
 
         DocumentProto.Builder strippedDocumentProto = documentProto.toBuilder();
-        AppSearchImpl.removePrefixesFromDocument(strippedDocumentProto);
+        removePrefixesFromDocument(strippedDocumentProto);
         SearchResultPage searchResultPage =
-                AppSearchImpl.rewriteSearchResultProto(searchResultProto);
+                AppSearchImpl.rewriteSearchResultProto(searchResultProto, schemaMap);
         for (SearchResult result : searchResultPage.getResults()) {
             assertThat(result.getPackageName()).isEqualTo("com.package.foo");
-            assertThat(result.getDocument()).isEqualTo(
+            assertThat(result.getDatabaseName()).isEqualTo("databaseName");
+            assertThat(result.getGenericDocument()).isEqualTo(
                     GenericDocumentToProtoConverter.toGenericDocument(
-                            strippedDocumentProto.build()));
+                            strippedDocumentProto.build(), prefix, schemaMap.get(prefix)));
         }
     }
+
+    @Test
+    public void testReportUsage() throws Exception {
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two docs
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "id1", "type").build();
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "id2", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package", "database", document2, /*logger=*/ null);
+
+        // Report some usages. id1 has 2 app and 1 system usage, id2 has 1 app and 2 system usage.
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id1", /*usageTimestampMillis=*/ 10, /*systemUsage=*/ false);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id1", /*usageTimestampMillis=*/ 20, /*systemUsage=*/ false);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id1", /*usageTimestampMillis=*/ 1000, /*systemUsage=*/ true);
+
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id2", /*usageTimestampMillis=*/ 100, /*systemUsage=*/ false);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id2", /*usageTimestampMillis=*/ 200, /*systemUsage=*/ true);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id2", /*usageTimestampMillis=*/ 150, /*systemUsage=*/ true);
+
+        // Sort by app usage count: id1 should win
+        List<SearchResult> page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
+                        .build(), /*logger=*/ null).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
+
+        // Sort by app usage timestamp: id2 should win
+        page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
+                        .build(), /*logger=*/ null).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
+
+        // Sort by system usage count: id2 should win
+        page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT)
+                        .build(), /*logger=*/ null).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
+
+        // Sort by system usage timestamp: id1 should win
+        page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(
+                                SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP)
+                        .build(), /*logger=*/ null).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
+    }
+
+    @Test
+    public void testGetStorageInfoForPackage_nonexistentPackage() throws Exception {
+        // "package2" doesn't exist yet, so it shouldn't have any storage size
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("nonexistent.package");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForPackage_withoutDocument() throws Exception {
+        // Insert schema for "package1"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Since "package1" doesn't have a document, it get any space attributed to it.
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("package1");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForPackage_proportionalToDocuments() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+        // Insert schema for "package1"
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert document for "package1"
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id1", "type").build();
+        mAppSearchImpl.putDocument("package1", "database", document, /*logger=*/ null);
+
+        // Insert schema for "package2"
+        mAppSearchImpl.setSchema(
+                "package2",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert two documents for "package2"
+        document = new GenericDocument.Builder<>("namespace", "id1", "type").build();
+        mAppSearchImpl.putDocument("package2", "database", document, /*logger=*/ null);
+        document = new GenericDocument.Builder<>("namespace", "id2", "type").build();
+        mAppSearchImpl.putDocument("package2", "database", document, /*logger=*/ null);
+
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("package1");
+        long size1 = storageInfo.getSizeBytes();
+        assertThat(size1).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(1);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        storageInfo = mAppSearchImpl.getStorageInfoForPackage("package2");
+        long size2 = storageInfo.getSizeBytes();
+        assertThat(size2).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(2);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        // Size is proportional to number of documents. Since "package2" has twice as many
+        // documents as "package1", its size is twice as much too.
+        assertThat(size2).isAtLeast(2 * size1);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_nonexistentPackage() throws Exception {
+        // "package2" doesn't exist yet, so it shouldn't have any storage size
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("nonexistent.package",
+                "nonexistentDatabase");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_nonexistentDatabase() throws Exception {
+        // Insert schema for "package1"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // "package2" doesn't exist yet, so it shouldn't have any storage size
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1",
+                "nonexistentDatabase");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_withoutDocument() throws Exception {
+        // Insert schema for "package1"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Since "package1", "database1" doesn't have a document, it get any space attributed to it.
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database1");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_proportionalToDocuments() throws Exception {
+        // Insert schema for "package1", "database1" and "database2"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database2",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add a document for "package1", "database1"
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package1", "database1", document, /*logger=*/ null);
+
+        // Add two documents for "package1", "database2"
+        document = new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package1", "database2", document, /*logger=*/ null);
+        document = new GenericDocument.Builder<>("namespace1", "id2", "type").build();
+        mAppSearchImpl.putDocument("package1", "database2", document, /*logger=*/ null);
+
+
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database1");
+        long size1 = storageInfo.getSizeBytes();
+        assertThat(size1).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(1);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database2");
+        long size2 = storageInfo.getSizeBytes();
+        assertThat(size2).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(2);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        // Size is proportional to number of documents. Since "database2" has twice as many
+        // documents as "database1", its size is twice as much too.
+        assertThat(size2).isAtLeast(2 * size1);
+    }
+
+    @Test
+    public void testThrowsExceptionIfClosed() throws Exception {
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        // Initial check that we could do something at first.
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        appSearchImpl.close();
+
+        // Check all our public APIs
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.getSchema(
+                "package", "database"));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id", "type").build(),
+                /*logger=*/ null));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.getDocument(
+                "package", "database", "namespace", "id", Collections.emptyMap()));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.query(
+                "package",
+                "database",
+                "query",
+                new SearchSpec.Builder().build(),
+                /*logger=*/ null));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.globalQuery(
+                "query",
+                new SearchSpec.Builder().build(),
+                "package",
+                /*visibilityStore=*/ null,
+                Process.INVALID_UID,
+                /*callerHasSystemAccess=*/ false,
+                /*logger=*/ null));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.getNextPage("package",
+                /*nextPageToken=*/ 1L));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.invalidateNextPageToken(
+                "package",
+                /*nextPageToken=*/ 1L));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.reportUsage(
+                "package", "database", "namespace", "id",
+                /*usageTimestampMillis=*/ 1000L, /*systemUsage=*/ false));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.remove(
+                "package", "database", "namespace", "id", /*removeStatsBuilder=*/ null));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.removeByQuery(
+                "package",
+                "database",
+                "query",
+                new SearchSpec.Builder().build(),
+                /*removeStatsBuilder=*/ null));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.getStorageInfoForPackage(
+                "package"));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.getStorageInfoForDatabase(
+                "package", "database"));
+
+        assertThrows(IllegalStateException.class, () -> appSearchImpl.persistToDisk(
+                PersistType.Code.FULL));
+    }
+
+    @Test
+    public void testPutPersistsWithLiteFlush() throws Exception {
+        // Setup the index
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add a document and persist it.
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        appSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        GenericDocument getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document);
+
+        // That document should be visible even from another instance.
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document);
+    }
+
+    @Test
+    public void testDeletePersistsWithLiteFlush() throws Exception {
+        // Setup the index
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add two documents and persist them.
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        appSearchImpl.putDocument("package", "database", document1, /*logger=*/null);
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace1", "id2", "type").build();
+        appSearchImpl.putDocument("package", "database", document2, /*logger=*/null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        GenericDocument getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document1);
+        getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Delete the first document
+        appSearchImpl.remove("package", "database", "namespace1", "id1", /*statsBuilder=*/ null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+        assertThrows(AppSearchException.class, () -> appSearchImpl.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Only the second document should be retrievable from another instance.
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+        assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+    }
+
+    @Test
+    public void testDeleteByQueryPersistsWithLiteFlush() throws Exception {
+        // Setup the index
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add two documents and persist them.
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        appSearchImpl.putDocument("package", "database", document1, /*logger=*/null);
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace2", "id2", "type").build();
+        appSearchImpl.putDocument("package", "database", document2, /*logger=*/null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        GenericDocument getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document1);
+        getResult = appSearchImpl.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Delete the first document
+        appSearchImpl.removeByQuery("package", "database", "",
+                new SearchSpec.Builder().addFilterNamespaces("namespace1").setTermMatch(
+                        SearchSpec.TERM_MATCH_EXACT_ONLY).build(), /*statsBuilder=*/ null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+        assertThrows(AppSearchException.class, () -> appSearchImpl.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Only the second document should be retrievable from another instance.
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+        assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+    }
+
+    @Test
+    public void testGetIcingSearchEngineStorageInfo() throws Exception {
+        // Setup the index
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                appsearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add two documents
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        appSearchImpl.putDocument("package", "database", document1, /*logger=*/null);
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace1", "id2", "type").build();
+        appSearchImpl.putDocument("package", "database", document2, /*logger=*/null);
+
+        StorageInfoProto storageInfo = appSearchImpl.getRawStorageInfoProto();
+
+        // Simple checks to verify if we can get correct StorageInfoProto from IcingSearchEngine
+        // No need to cover all the fields
+        assertThat(storageInfo.getTotalStorageSize()).isGreaterThan(0);
+        assertThat(
+                storageInfo.getDocumentStorageInfo().getNumAliveDocuments())
+                .isEqualTo(2);
+        assertThat(
+                storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes())
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void testLimitConfig_DocumentSize() throws Exception {
+        // Create a new mAppSearchImpl with a lower limit
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return 80;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 1;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert a document which is too large
+        GenericDocument document = new GenericDocument.Builder<>(
+                "this_namespace_is_long_to_make_the_doc_big", "id", "type").build();
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Document \"id\" for package \"package\" serialized to 99 bytes, which exceeds"
+                        + " limit of 80 bytes");
+
+        // Make sure this failure didn't increase our document count. We should still be able to
+        // index 1 document.
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "id2", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document2, /*logger=*/ null);
+
+        // Now we should get a failure
+        GenericDocument document3 =
+                new GenericDocument.Builder<>("namespace", "id3", "type").build();
+        e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document3, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 1 documents");
+    }
+
+    @Test
+    public void testLimitConfig_Init() throws Exception {
+        // Create a new mAppSearchImpl with a lower limit
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return 80;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 1;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Index a document
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id1", "type").build(),
+                /*logger=*/ null);
+
+        // Now we should get a failure
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "id2", "type").build();
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document2, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 1 documents");
+
+        // Close and reinitialize AppSearchImpl
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return 80;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 1;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Make sure the limit is maintained
+        e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document2, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 1 documents");
+    }
+
+    @Test
+    public void testLimitConfig_Remove() throws Exception {
+        // Create a new mAppSearchImpl with a lower limit
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return Integer.MAX_VALUE;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 3;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Index 3 documents
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id1", "type").build(),
+                /*logger=*/ null);
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id2", "type").build(),
+                /*logger=*/ null);
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id3", "type").build(),
+                /*logger=*/ null);
+
+        // Now we should get a failure
+        GenericDocument document4 =
+                new GenericDocument.Builder<>("namespace", "id4", "type").build();
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document4, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 3 documents");
+
+        // Remove a document that doesn't exist
+        assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.remove(
+                        "package", "database", "namespace", "id4", /*removeStatsBuilder=*/null));
+
+        // Should still fail
+        e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document4, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 3 documents");
+
+        // Remove a document that does exist
+        mAppSearchImpl.remove(
+                "package", "database", "namespace", "id2", /*removeStatsBuilder=*/null);
+
+        // Now doc4 should work
+        mAppSearchImpl.putDocument("package", "database", document4, /*logger=*/ null);
+
+        // The next one should fail again
+        e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id5", "type").build(),
+                /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 3 documents");
+    }
+
+    @Test
+    public void testLimitConfig_DifferentPackages() throws Exception {
+        // Create a new mAppSearchImpl with a lower limit
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return Integer.MAX_VALUE;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 2;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database2",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package2",
+                "database1",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package2",
+                "database2",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Index documents in package1/database1
+        mAppSearchImpl.putDocument(
+                "package1",
+                "database1",
+                new GenericDocument.Builder<>("namespace", "id1", "type").build(),
+                /*logger=*/ null);
+        mAppSearchImpl.putDocument(
+                "package1",
+                "database2",
+                new GenericDocument.Builder<>("namespace", "id2", "type").build(),
+                /*logger=*/ null);
+
+        // Indexing a third doc into package1 should fail (here we use database3)
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument(
+                        "package1",
+                        "database3",
+                        new GenericDocument.Builder<>("namespace", "id3", "type").build(),
+                        /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package1\" exceeded limit of 2 documents");
+
+        // Indexing a doc into package2 should succeed
+        mAppSearchImpl.putDocument(
+                "package2",
+                "database1",
+                new GenericDocument.Builder<>("namespace", "id1", "type").build(),
+                /*logger=*/ null);
+
+        // Reinitialize to make sure packages are parsed correctly on init
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return Integer.MAX_VALUE;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 2;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // package1 should still be out of space
+        e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument(
+                        "package1",
+                        "database4",
+                        new GenericDocument.Builder<>("namespace", "id4", "type").build(),
+                        /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package1\" exceeded limit of 2 documents");
+
+        // package2 has room for one more
+        mAppSearchImpl.putDocument(
+                "package2",
+                "database2",
+                new GenericDocument.Builder<>("namespace", "id2", "type").build(),
+                /*logger=*/ null);
+
+        // now package2 really is out of space
+        e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument(
+                        "package2",
+                        "database3",
+                        new GenericDocument.Builder<>("namespace", "id3", "type").build(),
+                        /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package2\" exceeded limit of 2 documents");
+    }
+
+    @Test
+    public void testLimitConfig_RemoveByQyery() throws Exception {
+        // Create a new mAppSearchImpl with a lower limit
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return Integer.MAX_VALUE;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 3;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Insert schema
+        List<AppSearchSchema> schemas = Collections.singletonList(
+                new AppSearchSchema.Builder("type")
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                                .setIndexingType(
+                                        AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .setTokenizerType(
+                                        AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                .build())
+                        .build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Index 3 documents
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id1", "type")
+                        .setPropertyString("body", "tablet")
+                        .build(),
+                /*logger=*/ null);
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id2", "type")
+                        .setPropertyString("body", "tabby")
+                        .build(),
+                /*logger=*/ null);
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id3", "type")
+                        .setPropertyString("body", "grabby")
+                        .build(),
+                /*logger=*/ null);
+
+        // Now we should get a failure
+        GenericDocument document4 =
+                new GenericDocument.Builder<>("namespace", "id4", "type").build();
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document4, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 3 documents");
+
+        // Run removebyquery, deleting nothing
+        mAppSearchImpl.removeByQuery(
+                "package",
+                "database",
+                "nothing",
+                new SearchSpec.Builder().build(),
+                /*removeStatsBuilder=*/null);
+
+        // Should still fail
+        e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document4, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 3 documents");
+
+        // Remove "tab*"
+        mAppSearchImpl.removeByQuery(
+                "package",
+                "database",
+                "tab",
+                new SearchSpec.Builder().build(),
+                /*removeStatsBuilder=*/null);
+
+        // Now doc4 and doc5 should work
+        mAppSearchImpl.putDocument("package", "database", document4, /*logger=*/ null);
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id5", "type").build(),
+                /*logger=*/ null);
+
+        // We only deleted 2 docs so the next one should fail again
+        e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id6", "type").build(),
+                /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 3 documents");
+    }
+
+    @Test
+    public void testLimitConfig_Replace() throws Exception {
+        // Create a new mAppSearchImpl with a lower limit
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return Integer.MAX_VALUE;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 2;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Insert schema
+        List<AppSearchSchema> schemas = Collections.singletonList(
+                new AppSearchSchema.Builder("type")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("body").build())
+                        .build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Index a document
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id1", "type")
+                        .setPropertyString("body", "id1.orig")
+                        .build(),
+                /*logger=*/ null);
+        // Replace it with another doc
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id1", "type")
+                        .setPropertyString("body", "id1.new")
+                        .build(),
+                /*logger=*/ null);
+
+        // Index id2. This should pass but only because we check for replacements.
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id2", "type").build(),
+                /*logger=*/ null);
+
+        // Now we should get a failure on id3
+        GenericDocument document3 =
+                new GenericDocument.Builder<>("namespace", "id3", "type").build();
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document3, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 2 documents");
+    }
+
+    @Test
+    public void testLimitConfig_ReplaceReinit() throws Exception {
+        // Create a new mAppSearchImpl with a lower limit
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return Integer.MAX_VALUE;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 2;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Insert schema
+        List<AppSearchSchema> schemas = Collections.singletonList(
+                new AppSearchSchema.Builder("type")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("body").build())
+                        .build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Index a document
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id1", "type")
+                        .setPropertyString("body", "id1.orig")
+                        .build(),
+                /*logger=*/ null);
+        // Replace it with another doc
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id1", "type")
+                        .setPropertyString("body", "id1.new")
+                        .build(),
+                /*logger=*/ null);
+
+        // Reinitialize to make sure replacements are correctly accounted for by init
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new LimitConfig() {
+                    @Override
+                    public int getMaxDocumentSizeBytes() {
+                        return Integer.MAX_VALUE;
+                    }
+
+                    @Override
+                    public int getMaxDocumentCount() {
+                        return 2;
+                    }
+                },
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        // Index id2. This should pass but only because we check for replacements.
+        mAppSearchImpl.putDocument(
+                "package",
+                "database",
+                new GenericDocument.Builder<>("namespace", "id2", "type").build(),
+                /*logger=*/ null);
+
+        // Now we should get a failure on id3
+        GenericDocument document3 =
+                new GenericDocument.Builder<>("namespace", "id3", "type").build();
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.putDocument("package", "database", document3, /*logger=*/ null));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
+        assertThat(e).hasMessageThat().contains(
+                "Package \"package\" exceeded limit of 2 documents");
+    }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
new file mode 100644
index 0000000..a0dbd6c
--- /dev/null
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
@@ -0,0 +1,861 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.stats.CallStats;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.OptimizeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchStats;
+import androidx.appsearch.localstorage.stats.SetSchemaStats;
+
+import com.google.android.icing.proto.DeleteStatsProto;
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.InitializeStatsProto;
+import com.google.android.icing.proto.OptimizeStatsProto;
+import com.google.android.icing.proto.PutDocumentStatsProto;
+import com.google.android.icing.proto.PutResultProto;
+import com.google.android.icing.proto.QueryStatsProto;
+import com.google.android.icing.proto.ScoringSpecProto;
+import com.google.android.icing.proto.SetSchemaResultProto;
+import com.google.android.icing.proto.StatusProto;
+import com.google.android.icing.proto.TermMatchType;
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+public class AppSearchLoggerTest {
+    private static final String PACKAGE_NAME = "packageName";
+    private static final String DATABASE = "database";
+    /**
+     * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
+     */
+    private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+    @Rule
+    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+    private AppSearchImpl mAppSearchImpl;
+    private TestLogger mLogger;
+
+    @Before
+    public void setUp() throws Exception {
+        mAppSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+        mLogger = new TestLogger();
+    }
+
+    // Test only not thread safe.
+    public static class TestLogger implements AppSearchLogger {
+        @Nullable
+        CallStats mCallStats;
+        @Nullable
+        PutDocumentStats mPutDocumentStats;
+        @Nullable
+        InitializeStats mInitializeStats;
+        @Nullable
+        SearchStats mSearchStats;
+        @Nullable
+        RemoveStats mRemoveStats;
+        @Nullable
+        OptimizeStats mOptimizeStats;
+        @Nullable
+        SetSchemaStats mSetSchemaStats;
+
+        @Override
+        public void logStats(@NonNull CallStats stats) {
+            mCallStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull PutDocumentStats stats) {
+            mPutDocumentStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull InitializeStats stats) {
+            mInitializeStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull SearchStats stats) {
+            mSearchStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull RemoveStats stats) {
+            mRemoveStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull OptimizeStats stats) {
+            mOptimizeStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull SetSchemaStats stats) {
+            mSetSchemaStats = stats;
+        }
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_initialize() {
+        int nativeLatencyMillis = 3;
+        int nativeDocumentStoreRecoveryCause = InitializeStatsProto.RecoveryCause.DATA_LOSS_VALUE;
+        int nativeIndexRestorationCause =
+                InitializeStatsProto.RecoveryCause.INCONSISTENT_WITH_GROUND_TRUTH_VALUE;
+        int nativeSchemaStoreRecoveryCause =
+                InitializeStatsProto.RecoveryCause.SCHEMA_CHANGES_OUT_OF_SYNC_VALUE;
+        int nativeDocumentStoreRecoveryLatencyMillis = 7;
+        int nativeIndexRestorationLatencyMillis = 8;
+        int nativeSchemaStoreRecoveryLatencyMillis = 9;
+        int nativeDocumentStoreDataStatus =
+                InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE;
+        int nativeNumDocuments = 11;
+        int nativeNumSchemaTypes = 12;
+        InitializeStatsProto.Builder nativeInitBuilder = InitializeStatsProto.newBuilder()
+                .setLatencyMs(nativeLatencyMillis)
+                .setDocumentStoreRecoveryCause(InitializeStatsProto.RecoveryCause.forNumber(
+                        nativeDocumentStoreRecoveryCause))
+                .setIndexRestorationCause(
+                        InitializeStatsProto.RecoveryCause.forNumber(nativeIndexRestorationCause))
+                .setSchemaStoreRecoveryCause(
+                        InitializeStatsProto.RecoveryCause.forNumber(
+                                nativeSchemaStoreRecoveryCause))
+                .setDocumentStoreRecoveryLatencyMs(nativeDocumentStoreRecoveryLatencyMillis)
+                .setIndexRestorationLatencyMs(nativeIndexRestorationLatencyMillis)
+                .setSchemaStoreRecoveryLatencyMs(nativeSchemaStoreRecoveryLatencyMillis)
+                .setDocumentStoreDataStatus(InitializeStatsProto.DocumentStoreDataStatus.forNumber(
+                        nativeDocumentStoreDataStatus))
+                .setNumDocuments(nativeNumDocuments)
+                .setNumSchemaTypes(nativeNumSchemaTypes);
+        InitializeStats.Builder initBuilder = new InitializeStats.Builder();
+
+        AppSearchLoggerHelper.copyNativeStats(nativeInitBuilder.build(), initBuilder);
+
+        InitializeStats iStats = initBuilder.build();
+        assertThat(iStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(iStats.getDocumentStoreRecoveryCause()).isEqualTo(
+                nativeDocumentStoreRecoveryCause);
+        assertThat(iStats.getIndexRestorationCause()).isEqualTo(nativeIndexRestorationCause);
+        assertThat(iStats.getSchemaStoreRecoveryCause()).isEqualTo(
+                nativeSchemaStoreRecoveryCause);
+        assertThat(iStats.getDocumentStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeDocumentStoreRecoveryLatencyMillis);
+        assertThat(iStats.getIndexRestorationLatencyMillis()).isEqualTo(
+                nativeIndexRestorationLatencyMillis);
+        assertThat(iStats.getSchemaStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeSchemaStoreRecoveryLatencyMillis);
+        assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
+                nativeDocumentStoreDataStatus);
+        assertThat(iStats.getDocumentCount()).isEqualTo(nativeNumDocuments);
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(nativeNumSchemaTypes);
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_putDocument() {
+        final int nativeLatencyMillis = 3;
+        final int nativeDocumentStoreLatencyMillis = 4;
+        final int nativeIndexLatencyMillis = 5;
+        final int nativeIndexMergeLatencyMillis = 6;
+        final int nativeDocumentSize = 7;
+        final int nativeNumTokensIndexed = 8;
+        final boolean nativeExceededMaxNumTokens = true;
+        PutDocumentStatsProto nativePutDocumentStats = PutDocumentStatsProto.newBuilder()
+                .setLatencyMs(nativeLatencyMillis)
+                .setDocumentStoreLatencyMs(nativeDocumentStoreLatencyMillis)
+                .setIndexLatencyMs(nativeIndexLatencyMillis)
+                .setIndexMergeLatencyMs(nativeIndexMergeLatencyMillis)
+                .setDocumentSize(nativeDocumentSize)
+                .setTokenizationStats(PutDocumentStatsProto.TokenizationStats.newBuilder()
+                        .setNumTokensIndexed(nativeNumTokensIndexed)
+                        .setExceededMaxTokenNum(nativeExceededMaxNumTokens)
+                        .build())
+                .build();
+        PutDocumentStats.Builder pBuilder = new PutDocumentStats.Builder(PACKAGE_NAME, DATABASE);
+
+        AppSearchLoggerHelper.copyNativeStats(nativePutDocumentStats, pBuilder);
+
+        PutDocumentStats pStats = pBuilder.build();
+        assertThat(pStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(pStats.getNativeDocumentStoreLatencyMillis()).isEqualTo(
+                nativeDocumentStoreLatencyMillis);
+        assertThat(pStats.getNativeIndexLatencyMillis()).isEqualTo(nativeIndexLatencyMillis);
+        assertThat(pStats.getNativeIndexMergeLatencyMillis()).isEqualTo(
+                nativeIndexMergeLatencyMillis);
+        assertThat(pStats.getNativeDocumentSizeBytes()).isEqualTo(nativeDocumentSize);
+        assertThat(pStats.getNativeNumTokensIndexed()).isEqualTo(nativeNumTokensIndexed);
+        assertThat(pStats.getNativeExceededMaxNumTokens()).isEqualTo(nativeExceededMaxNumTokens);
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_search() {
+        int nativeLatencyMillis = 4;
+        int nativeNumTerms = 5;
+        int nativeQueryLength = 6;
+        int nativeNumNamespacesFiltered = 7;
+        int nativeNumSchemaTypesFiltered = 8;
+        int nativeRequestedPageSize = 9;
+        int nativeNumResultsReturnedCurrentPage = 10;
+        boolean nativeIsFirstPage = true;
+        int nativeParseQueryLatencyMillis = 11;
+        int nativeRankingStrategy = ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP_VALUE;
+        int nativeNumDocumentsScored = 13;
+        int nativeScoringLatencyMillis = 14;
+        int nativeRankingLatencyMillis = 15;
+        int nativeNumResultsWithSnippets = 16;
+        int nativeDocumentRetrievingLatencyMillis = 17;
+        QueryStatsProto nativeQueryStats = QueryStatsProto.newBuilder()
+                .setLatencyMs(nativeLatencyMillis)
+                .setNumTerms(nativeNumTerms)
+                .setQueryLength(nativeQueryLength)
+                .setNumNamespacesFiltered(nativeNumNamespacesFiltered)
+                .setNumSchemaTypesFiltered(nativeNumSchemaTypesFiltered)
+                .setRequestedPageSize(nativeRequestedPageSize)
+                .setNumResultsReturnedCurrentPage(nativeNumResultsReturnedCurrentPage)
+                .setIsFirstPage(nativeIsFirstPage)
+                .setParseQueryLatencyMs(nativeParseQueryLatencyMillis)
+                .setRankingStrategy(
+                        ScoringSpecProto.RankingStrategy.Code.forNumber(nativeRankingStrategy))
+                .setNumDocumentsScored(nativeNumDocumentsScored)
+                .setScoringLatencyMs(nativeScoringLatencyMillis)
+                .setRankingLatencyMs(nativeRankingLatencyMillis)
+                .setNumResultsWithSnippets(nativeNumResultsWithSnippets)
+                .setDocumentRetrievalLatencyMs(nativeDocumentRetrievingLatencyMillis)
+                .build();
+        SearchStats.Builder qBuilder = new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL,
+                PACKAGE_NAME).setDatabase(DATABASE);
+
+        AppSearchLoggerHelper.copyNativeStats(nativeQueryStats, qBuilder);
+
+        SearchStats sStats = qBuilder.build();
+        assertThat(sStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(sStats.getTermCount()).isEqualTo(nativeNumTerms);
+        assertThat(sStats.getQueryLength()).isEqualTo(nativeQueryLength);
+        assertThat(sStats.getFilteredNamespaceCount()).isEqualTo(nativeNumNamespacesFiltered);
+        assertThat(sStats.getFilteredSchemaTypeCount()).isEqualTo(
+                nativeNumSchemaTypesFiltered);
+        assertThat(sStats.getRequestedPageSize()).isEqualTo(nativeRequestedPageSize);
+        assertThat(sStats.getCurrentPageReturnedResultCount()).isEqualTo(
+                nativeNumResultsReturnedCurrentPage);
+        assertThat(sStats.isFirstPage()).isTrue();
+        assertThat(sStats.getParseQueryLatencyMillis()).isEqualTo(
+                nativeParseQueryLatencyMillis);
+        assertThat(sStats.getRankingStrategy()).isEqualTo(nativeRankingStrategy);
+        assertThat(sStats.getScoredDocumentCount()).isEqualTo(nativeNumDocumentsScored);
+        assertThat(sStats.getScoringLatencyMillis()).isEqualTo(nativeScoringLatencyMillis);
+        assertThat(sStats.getRankingLatencyMillis()).isEqualTo(nativeRankingLatencyMillis);
+        assertThat(sStats.getResultWithSnippetsCount()).isEqualTo(nativeNumResultsWithSnippets);
+        assertThat(sStats.getDocumentRetrievingLatencyMillis()).isEqualTo(
+                nativeDocumentRetrievingLatencyMillis);
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_remove() {
+        final int nativeLatencyMillis = 1;
+        final int nativeDeleteType = 2;
+        final int nativeNumDocumentDeleted = 3;
+        DeleteStatsProto nativeDeleteStatsProto = DeleteStatsProto.newBuilder()
+                .setLatencyMs(nativeLatencyMillis)
+                .setDeleteType(DeleteStatsProto.DeleteType.Code.forNumber(nativeDeleteType))
+                .setNumDocumentsDeleted(nativeNumDocumentDeleted)
+                .build();
+        RemoveStats.Builder rBuilder = new RemoveStats.Builder(
+                "packageName",
+                "database");
+
+        AppSearchLoggerHelper.copyNativeStats(nativeDeleteStatsProto, rBuilder);
+
+        RemoveStats rStats = rBuilder.build();
+        assertThat(rStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(rStats.getDeleteType()).isEqualTo(nativeDeleteType);
+        assertThat(rStats.getDeletedDocumentCount()).isEqualTo(nativeNumDocumentDeleted);
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_optimize() {
+        int nativeLatencyMillis = 1;
+        int nativeDocumentStoreOptimizeLatencyMillis = 2;
+        int nativeIndexRestorationLatencyMillis = 3;
+        int nativeNumOriginalDocuments = 4;
+        int nativeNumDeletedDocuments = 5;
+        int nativeNumExpiredDocuments = 6;
+        long nativeStorageSizeBeforeBytes = Integer.MAX_VALUE + 1;
+        long nativeStorageSizeAfterBytes = Integer.MAX_VALUE + 2;
+        long nativeTimeSinceLastOptimizeMillis = Integer.MAX_VALUE + 3;
+        OptimizeStatsProto optimizeStatsProto = OptimizeStatsProto.newBuilder()
+                .setLatencyMs(nativeLatencyMillis)
+                .setDocumentStoreOptimizeLatencyMs(nativeDocumentStoreOptimizeLatencyMillis)
+                .setIndexRestorationLatencyMs(nativeIndexRestorationLatencyMillis)
+                .setNumOriginalDocuments(nativeNumOriginalDocuments)
+                .setNumDeletedDocuments(nativeNumDeletedDocuments)
+                .setNumExpiredDocuments(nativeNumExpiredDocuments)
+                .setStorageSizeBefore(nativeStorageSizeBeforeBytes)
+                .setStorageSizeAfter(nativeStorageSizeAfterBytes)
+                .setTimeSinceLastOptimizeMs(nativeTimeSinceLastOptimizeMillis)
+                .build();
+        OptimizeStats.Builder oBuilder = new OptimizeStats.Builder();
+
+        AppSearchLoggerHelper.copyNativeStats(optimizeStatsProto, oBuilder);
+
+        OptimizeStats oStats = oBuilder.build();
+        assertThat(oStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(oStats.getDocumentStoreOptimizeLatencyMillis()).isEqualTo(
+                nativeDocumentStoreOptimizeLatencyMillis);
+        assertThat(oStats.getIndexRestorationLatencyMillis()).isEqualTo(
+                nativeIndexRestorationLatencyMillis);
+        assertThat(oStats.getOriginalDocumentCount()).isEqualTo(nativeNumOriginalDocuments);
+        assertThat(oStats.getDeletedDocumentCount()).isEqualTo(nativeNumDeletedDocuments);
+        assertThat(oStats.getExpiredDocumentCount()).isEqualTo(nativeNumExpiredDocuments);
+        assertThat(oStats.getStorageSizeBeforeBytes()).isEqualTo(nativeStorageSizeBeforeBytes);
+        assertThat(oStats.getStorageSizeAfterBytes()).isEqualTo(nativeStorageSizeAfterBytes);
+        assertThat(oStats.getTimeSinceLastOptimizeMillis()).isEqualTo(
+                nativeTimeSinceLastOptimizeMillis);
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_setSchema() {
+        ImmutableList<String> newSchemaTypeChangeList = ImmutableList.of("new1");
+        ImmutableList<String> deletedSchemaTypesList = ImmutableList.of("deleted1", "deleted2");
+        ImmutableList<String> compatibleTypesList = ImmutableList.of("compatible1", "compatible2");
+        ImmutableList<String> indexIncompatibleTypeChangeList = ImmutableList.of("index1");
+        ImmutableList<String> backwardsIncompatibleTypeChangeList = ImmutableList.of("backwards1");
+        SetSchemaResultProto setSchemaResultProto = SetSchemaResultProto.newBuilder()
+                .addAllNewSchemaTypes(newSchemaTypeChangeList)
+                .addAllDeletedSchemaTypes(deletedSchemaTypesList)
+                .addAllFullyCompatibleChangedSchemaTypes(compatibleTypesList)
+                .addAllIndexIncompatibleChangedSchemaTypes(indexIncompatibleTypeChangeList)
+                .addAllIncompatibleSchemaTypes(backwardsIncompatibleTypeChangeList)
+                .build();
+        SetSchemaStats.Builder sBuilder = new SetSchemaStats.Builder(PACKAGE_NAME, DATABASE);
+
+        AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto, sBuilder);
+
+        SetSchemaStats sStats = sBuilder.build();
+        assertThat(sStats.getNewTypeCount()).isEqualTo(newSchemaTypeChangeList.size());
+        assertThat(sStats.getDeletedTypeCount()).isEqualTo(deletedSchemaTypesList.size());
+        assertThat(sStats.getCompatibleTypeChangeCount()).isEqualTo(compatibleTypesList.size());
+        assertThat(sStats.getIndexIncompatibleTypeChangeCount()).isEqualTo(
+                indexIncompatibleTypeChangeList.size());
+        assertThat(sStats.getBackwardsIncompatibleTypeChangeCount()).isEqualTo(
+                backwardsIncompatibleTypeChangeList.size());
+    }
+
+    //
+    // Testing actual logging
+    //
+    @Test
+    public void testLoggingStats_initializeWithoutDocuments_success() throws Exception {
+        // Create an unused AppSearchImpl to generated an InitializeStats.
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
+        AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new UnlimitedLimitConfig(),
+                initStatsBuilder,
+                ALWAYS_OPTIMIZE);
+        InitializeStats iStats = initStatsBuilder.build();
+
+        assertThat(iStats).isNotNull();
+        assertThat(iStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        // Total latency captured in LocalStorage
+        assertThat(iStats.getTotalLatencyMillis()).isEqualTo(0);
+        assertThat(iStats.hasDeSync()).isFalse();
+        assertThat(iStats.getNativeLatencyMillis()).isGreaterThan(0);
+        assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
+                InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
+        assertThat(iStats.getDocumentCount()).isEqualTo(0);
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(0);
+        assertThat(iStats.hasReset()).isEqualTo(false);
+        assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+    }
+
+    @Test
+    public void testLoggingStats_initializeWithDocuments_success() throws Exception {
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        final File folder = mTemporaryFolder.newFolder();
+
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                folder,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE);
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("Type1").build(),
+                new AppSearchSchema.Builder("Type2").build());
+        appSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Type1").build();
+        GenericDocument doc2 =
+                new GenericDocument.Builder<>("namespace", "id2", "Type1").build();
+        appSearchImpl.putDocument(testPackageName, testDatabase, doc1, mLogger);
+        appSearchImpl.putDocument(testPackageName, testDatabase, doc2, mLogger);
+        appSearchImpl.close();
+
+        // Create another appsearchImpl on the same folder
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
+        AppSearchImpl.create(folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+        InitializeStats iStats = initStatsBuilder.build();
+
+        assertThat(iStats).isNotNull();
+        assertThat(iStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        // Total latency captured in LocalStorage
+        assertThat(iStats.getTotalLatencyMillis()).isEqualTo(0);
+        assertThat(iStats.hasDeSync()).isFalse();
+        assertThat(iStats.getNativeLatencyMillis()).isGreaterThan(0);
+        assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
+                InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
+        assertThat(iStats.getDocumentCount()).isEqualTo(2);
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(2);
+        assertThat(iStats.hasReset()).isEqualTo(false);
+        assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+    }
+
+    @Test
+    public void testLoggingStats_initialize_failure() throws Exception {
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        final File folder = mTemporaryFolder.newFolder();
+
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                folder, new UnlimitedLimitConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("Type1").build(),
+                new AppSearchSchema.Builder("Type2").build());
+        appSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Insert a valid doc
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Type1").build();
+        appSearchImpl.putDocument(testPackageName, testDatabase, doc1, mLogger);
+
+        // Insert the invalid doc with an invalid namespace right into icing
+        DocumentProto invalidDoc = DocumentProto.newBuilder()
+                .setNamespace("invalidNamespace")
+                .setUri("id2")
+                .setSchema(String.format("%s$%s/Type1", testPackageName, testDatabase))
+                .build();
+        PutResultProto putResultProto = appSearchImpl.mIcingSearchEngineLocked.put(invalidDoc);
+        assertThat(putResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK);
+        appSearchImpl.close();
+
+        // Create another appsearchImpl on the same folder
+        InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
+        AppSearchImpl.create(folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+        InitializeStats iStats = initStatsBuilder.build();
+
+        // Some of other fields are already covered by AppSearchImplTest#testReset()
+        assertThat(iStats).isNotNull();
+        assertThat(iStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_INTERNAL_ERROR);
+        assertThat(iStats.hasReset()).isTrue();
+    }
+
+    @Test
+    public void testLoggingStats_putDocument_success() throws Exception {
+        // Insert schema
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        AppSearchSchema testSchema = new AppSearchSchema.Builder("type")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        List<AppSearchSchema> schemas = Collections.singletonList(testSchema);
+        mAppSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "type")
+                        .setPropertyString("subject", "testPut example1")
+                        .build();
+
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document, mLogger);
+
+        PutDocumentStats pStats = mLogger.mPutDocumentStats;
+        assertThat(pStats).isNotNull();
+        assertThat(pStats.getPackageName()).isEqualTo(testPackageName);
+        assertThat(pStats.getDatabase()).isEqualTo(testDatabase);
+        assertThat(pStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        // The latency related native stats have been tested in testCopyNativeStats
+        assertThat(pStats.getNativeDocumentSizeBytes()).isGreaterThan(0);
+        assertThat(pStats.getNativeNumTokensIndexed()).isGreaterThan(0);
+    }
+
+    @Test
+    public void testLoggingStats_putDocument_failure() throws Exception {
+        // Insert schema
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        AppSearchSchema testSchema = new AppSearchSchema.Builder("type")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        List<AppSearchSchema> schemas = Collections.singletonList(testSchema);
+        mAppSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "type")
+                        .setPropertyString("nonExist", "testPut example1")
+                        .build();
+
+        AppSearchException exception = Assert.assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.putDocument(testPackageName, testDatabase, document, mLogger));
+        assertThat(exception.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        PutDocumentStats pStats = mLogger.mPutDocumentStats;
+        assertThat(pStats).isNotNull();
+        assertThat(pStats.getPackageName()).isEqualTo(testPackageName);
+        assertThat(pStats.getDatabase()).isEqualTo(testDatabase);
+        assertThat(pStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testLoggingStats_search_success() throws Exception {
+        // Insert schema
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        AppSearchSchema testSchema = new AppSearchSchema.Builder("type")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        List<AppSearchSchema> schemas = Collections.singletonList(testSchema);
+        mAppSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "id1", "type")
+                        .setPropertyString("subject", "testPut example1")
+                        .build();
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "id2", "type")
+                        .setPropertyString("subject", "testPut example2")
+                        .build();
+        GenericDocument document3 =
+                new GenericDocument.Builder<>("namespace", "id3", "type")
+                        .setPropertyString("subject", "testPut 3")
+                        .build();
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document1, mLogger);
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document2, mLogger);
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document3, mLogger);
+
+
+        // No query filters specified. package2 should only get its own documents back.
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
+                        .build();
+        String queryStr = "testPut e";
+        SearchResultPage searchResultPage = mAppSearchImpl.query(testPackageName, testDatabase,
+                queryStr, searchSpec, /*logger=*/ mLogger);
+
+        assertThat(searchResultPage.getResults()).hasSize(2);
+        // The ranking strategy is LIFO
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document2);
+        assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(document1);
+
+        SearchStats sStats = mLogger.mSearchStats;
+
+        assertThat(sStats).isNotNull();
+        assertThat(sStats.getPackageName()).isEqualTo(testPackageName);
+        assertThat(sStats.getDatabase()).isEqualTo(testDatabase);
+        assertThat(sStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        assertThat(sStats.getTotalLatencyMillis()).isGreaterThan(0);
+        assertThat(sStats.getVisibilityScope()).isEqualTo(SearchStats.VISIBILITY_SCOPE_LOCAL);
+        assertThat(sStats.getTermCount()).isEqualTo(2);
+        assertThat(sStats.getQueryLength()).isEqualTo(queryStr.length());
+        assertThat(sStats.getFilteredNamespaceCount()).isEqualTo(1);
+        assertThat(sStats.getFilteredSchemaTypeCount()).isEqualTo(1);
+        assertThat(sStats.getCurrentPageReturnedResultCount()).isEqualTo(2);
+        assertThat(sStats.isFirstPage()).isTrue();
+        assertThat(sStats.getRankingStrategy()).isEqualTo(
+                SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP);
+        assertThat(sStats.getScoredDocumentCount()).isEqualTo(2);
+        assertThat(sStats.getResultWithSnippetsCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testLoggingStats_search_failure() throws Exception {
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("Type1").build(),
+                new AppSearchSchema.Builder("Type2").build());
+        mAppSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
+                        .addFilterPackageNames("anotherPackage")
+                        .build();
+
+        mAppSearchImpl.query(testPackageName,
+                testPackageName,
+                /* queryExpression= */ "",
+                searchSpec, /*logger=*/ mLogger);
+
+        SearchStats sStats = mLogger.mSearchStats;
+        assertThat(sStats).isNotNull();
+        assertThat(sStats.getPackageName()).isEqualTo(testPackageName);
+        assertThat(sStats.getDatabase()).isEqualTo(testPackageName);
+        assertThat(sStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+    }
+
+    @Test
+    public void testLoggingStats_remove_success() throws Exception {
+        // Insert schema
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        final String testNamespace = "testNameSpace";
+        final String testId = "id";
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        GenericDocument document =
+                new GenericDocument.Builder<>(testNamespace, testId, "type").build();
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document, /*logger=*/ null);
+
+        RemoveStats.Builder rStatsBuilder = new RemoveStats.Builder(testPackageName, testDatabase);
+        mAppSearchImpl.remove(testPackageName, testDatabase, testNamespace, testId, rStatsBuilder);
+        RemoveStats rStats = rStatsBuilder.build();
+
+        assertThat(rStats.getPackageName()).isEqualTo(testPackageName);
+        assertThat(rStats.getDatabase()).isEqualTo(testDatabase);
+        // delete by namespace + id
+        assertThat(rStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        assertThat(rStats.getDeleteType()).isEqualTo(DeleteStatsProto.DeleteType.Code.SINGLE_VALUE);
+        assertThat(rStats.getDeletedDocumentCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testLoggingStats_remove_failure() throws Exception {
+        // Insert schema
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        final String testNamespace = "testNameSpace";
+        final String testId = "id";
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        GenericDocument document =
+                new GenericDocument.Builder<>(testNamespace, testId, "type").build();
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document, /*logger=*/ null);
+
+        RemoveStats.Builder rStatsBuilder = new RemoveStats.Builder(testPackageName, testDatabase);
+
+        AppSearchException exception = Assert.assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.remove(testPackageName, testDatabase, testNamespace,
+                        "invalidId", rStatsBuilder));
+        assertThat(exception.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        RemoveStats rStats = rStatsBuilder.build();
+        assertThat(rStats.getPackageName()).isEqualTo(testPackageName);
+        assertThat(rStats.getDatabase()).isEqualTo(testDatabase);
+        assertThat(rStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        // delete by namespace + id
+        assertThat(rStats.getDeleteType()).isEqualTo(DeleteStatsProto.DeleteType.Code.SINGLE_VALUE);
+        assertThat(rStats.getDeletedDocumentCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testLoggingStats_removeByQuery_success() throws Exception {
+        // Insert schema
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        final String testNamespace = "testNameSpace";
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(
+                testPackageName,
+                testDatabase,
+                schemas,
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        GenericDocument document1 =
+                new GenericDocument.Builder<>(testNamespace, "id1", "type").build();
+        GenericDocument document2 =
+                new GenericDocument.Builder<>(testNamespace, "id2", "type").build();
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document1, mLogger);
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document2, mLogger);
+        // No query filters specified. package2 should only get its own documents back.
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
+
+        RemoveStats.Builder rStatsBuilder = new RemoveStats.Builder(testPackageName, testDatabase);
+        mAppSearchImpl.removeByQuery(testPackageName, testDatabase,
+                /*queryExpression=*/"", searchSpec,
+                rStatsBuilder);
+        RemoveStats rStats = rStatsBuilder.build();
+
+        assertThat(rStats.getPackageName()).isEqualTo(testPackageName);
+        assertThat(rStats.getDatabase()).isEqualTo(testDatabase);
+        assertThat(rStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        // delete by query
+        assertThat(rStats.getDeleteType())
+                .isEqualTo(DeleteStatsProto.DeleteType.Code.DEPRECATED_QUERY_VALUE);
+        assertThat(rStats.getDeletedDocumentCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void testLoggingStats_setSchema() throws Exception {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mAppSearchImpl.setSchema(
+                PACKAGE_NAME,
+                DATABASE,
+                Collections.singletonList(schema1),
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // create a backwards incompatible schema
+        SetSchemaStats.Builder sStatsBuilder = new SetSchemaStats.Builder(PACKAGE_NAME, DATABASE);
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("testSchema").build();
+        mAppSearchImpl.setSchema(
+                PACKAGE_NAME,
+                DATABASE,
+                Collections.singletonList(schema2),
+                /*visibilityStore=*/ null,
+                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
+                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ sStatsBuilder);
+
+        SetSchemaStats sStats = sStatsBuilder.build();
+        assertThat(sStats.getPackageName()).isEqualTo(PACKAGE_NAME);
+        assertThat(sStats.getDatabase()).isEqualTo(DATABASE);
+        assertThat(sStats.getNewTypeCount()).isEqualTo(0);
+        assertThat(sStats.getCompatibleTypeChangeCount()).isEqualTo(0);
+        assertThat(sStats.getIndexIncompatibleTypeChangeCount()).isEqualTo(1);
+        assertThat(sStats.getBackwardsIncompatibleTypeChangeCount()).isEqualTo(1);
+    }
+}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/JetpackOptimizeStrategyTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/JetpackOptimizeStrategyTest.java
new file mode 100644
index 0000000..b33caba
--- /dev/null
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/JetpackOptimizeStrategyTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.localstorage;
+
+import static androidx.appsearch.localstorage.JetpackOptimizeStrategy.BYTES_OPTIMIZE_THRESHOLD;
+import static androidx.appsearch.localstorage.JetpackOptimizeStrategy.DOC_COUNT_OPTIMIZE_THRESHOLD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.icing.proto.GetOptimizeInfoResultProto;
+import com.google.android.icing.proto.StatusProto;
+
+import org.junit.Test;
+
+public class JetpackOptimizeStrategyTest {
+    JetpackOptimizeStrategy mJetpackOptimizeStrategy = new JetpackOptimizeStrategy();
+
+    @Test
+    public void testShouldOptimize_byteThreshold() {
+        GetOptimizeInfoResultProto optimizeInfo = GetOptimizeInfoResultProto.newBuilder()
+                .setTimeSinceLastOptimizeMs(0)
+                .setEstimatedOptimizableBytes(BYTES_OPTIMIZE_THRESHOLD)
+                .setOptimizableDocs(0)
+                .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
+                .build();
+        assertThat(mJetpackOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
+    }
+
+    @Test
+    public void testShouldNotOptimize_timeThreshold() {
+        GetOptimizeInfoResultProto optimizeInfo = GetOptimizeInfoResultProto.newBuilder()
+                .setTimeSinceLastOptimizeMs(Integer.MAX_VALUE)
+                .setEstimatedOptimizableBytes(0)
+                .setOptimizableDocs(0)
+                .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
+                .build();
+        assertThat(mJetpackOptimizeStrategy.shouldOptimize(optimizeInfo)).isFalse();
+    }
+
+    @Test
+    public void testShouldOptimize_docCountThreshold() {
+        GetOptimizeInfoResultProto optimizeInfo = GetOptimizeInfoResultProto.newBuilder()
+                .setTimeSinceLastOptimizeMs(0)
+                .setEstimatedOptimizableBytes(0)
+                .setOptimizableDocs(DOC_COUNT_OPTIMIZE_THRESHOLD)
+                .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
+                .build();
+        assertThat(mJetpackOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
+    }
+}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
index 254f8f7..333cea3 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
@@ -24,29 +24,68 @@
 
 import org.junit.Test;
 
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
 public class LocalStorageTest {
     @Test
     public void testSameInstance() throws Exception {
-        LocalStorage b1 =
-                LocalStorage.getOrCreateInstance(ApplicationProvider.getApplicationContext());
-        LocalStorage b2 =
-                LocalStorage.getOrCreateInstance(ApplicationProvider.getApplicationContext());
+        Executor executor = Executors.newCachedThreadPool();
+        LocalStorage b1 = LocalStorage.getOrCreateInstance(
+                ApplicationProvider.getApplicationContext(), executor, /*logger=*/ null);
+        LocalStorage b2 = LocalStorage.getOrCreateInstance(
+                ApplicationProvider.getApplicationContext(), executor, /*logger=*/ null);
         assertThat(b1).isSameInstanceAs(b2);
     }
 
     @Test
-    public void testDatabaseName() {
+    public void testSearchContext_databaseName() {
+        LocalStorage.SearchContext searchContext =
+                new LocalStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        /*databaseName=*/"dbName").build();
+
+        assertThat(searchContext.getDatabaseName()).isEqualTo("dbName");
+    }
+
+    @Test
+    public void testSearchContext_withClientExecutor() {
+        Executor executor = Executors.newSingleThreadExecutor();
+        LocalStorage.SearchContext searchContext = new LocalStorage.SearchContext.Builder(
+                ApplicationProvider.getApplicationContext(),
+                /*databaseName=*/"dbName")
+                .setWorkerExecutor(executor)
+                .build();
+
+        assertThat(searchContext.getWorkerExecutor()).isEqualTo(executor);
+        assertThat(searchContext.getDatabaseName()).isEqualTo("dbName");
+    }
+
+    @Test
+    public void testSearchContext_withDefaultExecutor() {
+        LocalStorage.SearchContext searchContext = new LocalStorage.SearchContext.Builder(
+                ApplicationProvider.getApplicationContext(),
+                /*databaseName=*/"dbName")
+                .build();
+
+        assertThat(searchContext.getWorkerExecutor()).isNotNull();
+        assertThat(searchContext.getDatabaseName()).isEqualTo("dbName");
+    }
+
+    @Test
+    public void testSearchContext_withInvalidDatabaseName() {
         // Test special character can present in database name. When a special character is banned
         // in database name, add checker in SearchContext.Builder and reflect it in java doc.
-        LocalStorage.SearchContext.Builder contextBuilder =
-                new LocalStorage.SearchContext.Builder(
-                        ApplicationProvider.getApplicationContext());
 
         IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> contextBuilder.setDatabaseName("testDatabaseNameEndWith/"));
+                () -> new LocalStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        "testDatabaseNameEndWith/").build());
         assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
         e = assertThrows(IllegalArgumentException.class,
-                () -> contextBuilder.setDatabaseName("/testDatabaseNameStartWith"));
+                () -> new LocalStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        "/testDatabaseNameStartWith").build());
         assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
deleted file mode 100644
index 4577f5a..0000000
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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 androidx.appsearch.localstorage;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
-import java.util.Collections;
-
-public class VisibilityStoreTest {
-
-    @Rule
-    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
-    private AppSearchImpl mAppSearchImpl;
-    private VisibilityStore mVisibilityStore;
-
-    @Before
-    public void setUp() throws Exception {
-        mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder());
-        mVisibilityStore = mAppSearchImpl.getVisibilityStoreLocked();
-    }
-
-    /**
-     * Make sure that we don't conflict with any special characters that AppSearchImpl has
-     * reserved.
-     */
-    @Test
-    public void testValidPackageName() {
-        assertThat(VisibilityStore.PACKAGE_NAME).doesNotContain(
-                "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences
-        assertThat(VisibilityStore.PACKAGE_NAME).doesNotContain(
-                "" + AppSearchImpl.DATABASE_DELIMITER); // Convert the chars to CharSequences
-    }
-
-    /**
-     * Make sure that we don't conflict with any special characters that AppSearchImpl has
-     * reserved.
-     */
-    @Test
-    public void testValidDatabaseName() {
-        assertThat(VisibilityStore.DATABASE_NAME).doesNotContain(
-                "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences
-        assertThat(VisibilityStore.DATABASE_NAME).doesNotContain(
-                "" + AppSearchImpl.DATABASE_DELIMITER); // Convert the chars to CharSequences
-    }
-
-    @Test
-    public void testSetVisibility() throws Exception {
-        mVisibilityStore.setVisibility("prefix",
-                /*schemasNotPlatformSurfaceable=*/
-                ImmutableSet.of("prefix/schema1", "prefix/schema2"));
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")).isFalse();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")).isFalse();
-
-        // New .setVisibility() call completely overrides previous visibility settings. So
-        // "schema2" isn't preserved.
-        mVisibilityStore.setVisibility("prefix",
-                /*schemasNotPlatformSurfaceable=*/
-                ImmutableSet.of("prefix/schema1", "prefix/schema3"));
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")).isFalse();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")).isTrue();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema3")).isFalse();
-
-        mVisibilityStore.setVisibility(
-                "prefix", /*schemasNotPlatformSurfaceable=*/ Collections.emptySet());
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")).isTrue();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")).isTrue();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema3")).isTrue();
-    }
-
-    @Test
-    public void testEmptyPrefix() throws Exception {
-        mVisibilityStore.setVisibility(/*prefix=*/ "",
-                /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2"));
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable(/*prefix=*/ "", "schema1")).isFalse();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable(/*prefix=*/ "", "schema2")).isFalse();
-    }
-}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index 1245262..f22818d 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -21,8 +21,11 @@
 import androidx.appsearch.app.GenericDocument;
 
 import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.protobuf.ByteString;
+import com.google.common.collect.ImmutableMap;
 
 import org.junit.Test;
 
@@ -30,29 +33,42 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 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 String SCHEMA_TYPE_1 = "sDocumentPropertiesSchemaType1";
+    private static final String SCHEMA_TYPE_2 = "sDocumentPropertiesSchemaType2";
     private static final GenericDocument DOCUMENT_PROPERTIES_1 =
             new GenericDocument.Builder<GenericDocument.Builder<?>>(
-                    "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
-            .setCreationTimestampMillis(12345L)
-            .build();
+                    "namespace", "sDocumentProperties1", SCHEMA_TYPE_1)
+                    .setCreationTimestampMillis(12345L)
+                    .build();
     private static final GenericDocument DOCUMENT_PROPERTIES_2 =
             new GenericDocument.Builder<GenericDocument.Builder<?>>(
-                    "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
-            .setCreationTimestampMillis(6789L)
+                    "namespace", "sDocumentProperties2", SCHEMA_TYPE_2)
+                    .setCreationTimestampMillis(6789L)
+                    .build();
+    private static final SchemaTypeConfigProto SCHEMA_PROTO_1 = SchemaTypeConfigProto.newBuilder()
+            .setSchemaType(SCHEMA_TYPE_1)
             .build();
+    private static final SchemaTypeConfigProto SCHEMA_PROTO_2 = SchemaTypeConfigProto.newBuilder()
+            .setSchemaType(SCHEMA_TYPE_2)
+            .build();
+    private static final String PREFIX = "package$databaseName/";
+    private static final Map<String, SchemaTypeConfigProto> SCHEMA_MAP =
+            ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, SCHEMA_PROTO_1, PREFIX + SCHEMA_TYPE_2,
+                    SCHEMA_PROTO_2);
 
     @Test
     public void testDocumentProtoConvert() {
         GenericDocument document =
-                new GenericDocument.Builder<GenericDocument.Builder<?>>("uri1", "schemaType1")
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
                         .setCreationTimestampMillis(5L)
                         .setScore(1)
                         .setTtlMillis(1L)
-                        .setNamespace("namespace")
                         .setPropertyLong("longKey1", 1L)
                         .setPropertyDouble("doubleKey1", 1.0)
                         .setPropertyBoolean("booleanKey1", true)
@@ -64,8 +80,8 @@
 
         // Create the Document proto. Need to sort the property order by key.
         DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
-                .setUri("uri1")
-                .setSchema("schemaType1")
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
                 .setCreationTimestampMs(5L)
                 .setScore(1)
                 .setTtlMs(1L)
@@ -95,9 +111,123 @@
             documentProtoBuilder.addProperties(propertyProtoMap.get(key));
         }
         DocumentProto documentProto = documentProtoBuilder.build();
-        assertThat(GenericDocumentToProtoConverter.toDocumentProto(document))
-                .isEqualTo(documentProto);
-        assertThat(document)
-                .isEqualTo(GenericDocumentToProtoConverter.toGenericDocument(documentProto));
+
+        GenericDocument convertedGenericDocument =
+                GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+                        SCHEMA_MAP);
+        DocumentProto convertedDocumentProto =
+                GenericDocumentToProtoConverter.toDocumentProto(document);
+
+        assertThat(convertedDocumentProto).isEqualTo(documentProto);
+        assertThat(convertedGenericDocument).isEqualTo(document);
+    }
+
+    @Test
+    public void testConvertDocument_whenPropertyHasEmptyList() {
+        String emptyStringPropertyName = "emptyStringProperty";
+        DocumentProto documentProto = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
+                .setCreationTimestampMs(5L)
+                .setNamespace("namespace")
+                .addProperties(
+                        PropertyProto.newBuilder()
+                                .setName(emptyStringPropertyName)
+                                .build()
+                ).build();
+
+        PropertyConfigProto emptyStringListProperty = PropertyConfigProto.newBuilder()
+                .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
+                .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                .setPropertyName(emptyStringPropertyName)
+                .build();
+        SchemaTypeConfigProto schemaTypeConfigProto = SchemaTypeConfigProto.newBuilder()
+                .addProperties(emptyStringListProperty)
+                .setSchemaType(SCHEMA_TYPE_1)
+                .build();
+        Map<String, SchemaTypeConfigProto> schemaMap =
+                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaTypeConfigProto);
+
+        GenericDocument convertedDocument = GenericDocumentToProtoConverter.toGenericDocument(
+                documentProto, PREFIX, schemaMap);
+
+        GenericDocument expectedDocument =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyString(emptyStringPropertyName)
+                        .build();
+        assertThat(convertedDocument).isEqualTo(expectedDocument);
+        assertThat(expectedDocument.getPropertyStringArray(emptyStringPropertyName)).isEmpty();
+    }
+
+    @Test
+    public void testConvertDocument_whenNestedDocumentPropertyHasEmptyList() {
+        String emptyStringPropertyName = "emptyStringProperty";
+        String documentPropertyName = "documentProperty";
+        DocumentProto nestedDocumentProto = DocumentProto.newBuilder()
+                .setUri("id2")
+                .setSchema(SCHEMA_TYPE_2)
+                .setCreationTimestampMs(5L)
+                .setNamespace("namespace")
+                .addProperties(
+                        PropertyProto.newBuilder()
+                                .setName(emptyStringPropertyName)
+                                .build()
+                ).build();
+        DocumentProto documentProto = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
+                .setCreationTimestampMs(5L)
+                .setNamespace("namespace")
+                .addProperties(
+                        PropertyProto.newBuilder()
+                                .addDocumentValues(nestedDocumentProto)
+                                .setName(documentPropertyName)
+                                .build()
+                ).build();
+
+        PropertyConfigProto documentProperty = PropertyConfigProto.newBuilder()
+                .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
+                .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+                .setPropertyName(documentPropertyName)
+                .setSchemaType(SCHEMA_TYPE_2)
+                .build();
+        SchemaTypeConfigProto schemaTypeConfigProto = SchemaTypeConfigProto.newBuilder()
+                .addProperties(documentProperty)
+                .setSchemaType(SCHEMA_TYPE_1)
+                .build();
+        PropertyConfigProto emptyStringListProperty = PropertyConfigProto.newBuilder()
+                .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
+                .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                .setPropertyName(emptyStringPropertyName)
+                .build();
+        SchemaTypeConfigProto nestedSchemaTypeConfigProto = SchemaTypeConfigProto.newBuilder()
+                .addProperties(emptyStringListProperty)
+                .setSchemaType(SCHEMA_TYPE_2)
+                .build();
+        Map<String, SchemaTypeConfigProto> schemaMap =
+                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaTypeConfigProto,
+                        PREFIX + SCHEMA_TYPE_2, nestedSchemaTypeConfigProto);
+
+        GenericDocument convertedDocument = GenericDocumentToProtoConverter.toGenericDocument(
+                documentProto, PREFIX, schemaMap);
+
+        GenericDocument expectedDocument =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyDocument(documentPropertyName,
+                                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace",
+                                        "id2", SCHEMA_TYPE_2)
+                                        .setCreationTimestampMillis(5L)
+                                        .setPropertyString(emptyStringPropertyName)
+                                        .build()
+                        )
+                        .build();
+        assertThat(convertedDocument).isEqualTo(expectedDocument);
+        assertThat(
+                expectedDocument.getPropertyDocument(documentPropertyName).getPropertyStringArray(
+                        emptyStringPropertyName)).isEmpty();
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 889b0c7..3f2cb70 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -31,22 +31,25 @@
     @Test
     public void testGetProto_Email() {
         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+                ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
 
         SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("Email")
+                .setVersion(12345)
                 .addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("subject")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
@@ -69,7 +72,7 @@
                         )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema, /*version=*/12345))
                 .isEqualTo(expectedEmailProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
                 .isEqualTo(emailSchema);
@@ -78,22 +81,21 @@
     @Test
     public void testGetProto_MusicRecording() {
         AppSearchSchema musicRecordingSchema = new AppSearchSchema.Builder("MusicRecording")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("artist")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("artist")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("pubDate")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+                ).addProperty(new AppSearchSchema.LongPropertyConfig.Builder("pubDate")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
                         .build()
                 ).build();
 
         SchemaTypeConfigProto expectedMusicRecordingProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("MusicRecording")
+                .setVersion(0)
                 .addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("artist")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
@@ -108,15 +110,10 @@
                         .setPropertyName("pubDate")
                         .setDataType(PropertyConfigProto.DataType.Code.INT64)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
-                        .setStringIndexingConfig(
-                                StringIndexingConfig.newBuilder()
-                                        .setTokenizerType(
-                                                StringIndexingConfig.TokenizerType.Code.NONE)
-                                        .setTermMatchType(TermMatchType.Code.UNKNOWN)
-                        )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(musicRecordingSchema))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(
+                musicRecordingSchema, /*version=*/0))
                 .isEqualTo(expectedMusicRecordingProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedMusicRecordingProto))
                 .isEqualTo(musicRecordingSchema);
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index 31bc328..c28e5af 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -20,9 +20,11 @@
 
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.localstorage.util.PrefixUtil;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SearchResultProto;
 import com.google.android.icing.proto.SnippetMatchProto;
 import com.google.android.icing.proto.SnippetProto;
@@ -30,189 +32,250 @@
 import org.junit.Test;
 
 import java.util.Collections;
+import java.util.Map;
 
 public class SnippetTest {
+    private static final String SCHEMA_TYPE = "schema1";
+    private static final String PACKAGE_NAME = "packageName";
+    private static final String DATABASE_NAME = "databaseName";
+    private static final String PREFIX = PrefixUtil.createPrefix(PACKAGE_NAME, DATABASE_NAME);
+    private static final SchemaTypeConfigProto SCHEMA_TYPE_CONFIG_PROTO =
+            SchemaTypeConfigProto.newBuilder()
+                    .setSchemaType(PREFIX + SCHEMA_TYPE)
+                    .build();
+    private static final Map<String, Map<String, SchemaTypeConfigProto>> SCHEMA_MAP =
+            Collections.singletonMap(PREFIX,
+                    Collections.singletonMap(PREFIX + SCHEMA_TYPE,
+                            SCHEMA_TYPE_CONFIG_PROTO));
 
-    // TODO(tytytyww): Add tests for Double and Long Snippets.
     @Test
     public void testSingleStringSnippet() {
-
         final String propertyKeyString = "content";
         final String propertyValueString = "A commonly used fake word is foo.\n"
                 + "   Another nonsense word that’s used a lot\n"
                 + "   is bar.\n";
-        final String uri = "uri1";
-        final String schemaType = "schema1";
-        final String searchWord = "foo";
+        final String id = "id1";
         final String exactMatch = "foo";
         final String window = "is foo";
 
         // Building the SearchResult received from query.
-        PropertyProto property = PropertyProto.newBuilder()
-                .setName(propertyKeyString)
-                .addStringValues(propertyValueString)
-                .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri(uri)
-                .setSchema(schemaType)
-                .addProperties(property)
+                .setUri(id)
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName(propertyKeyString)
+                        .addStringValues(propertyValueString))
                 .build();
         SnippetProto snippetProto = SnippetProto.newBuilder()
                 .addEntries(SnippetProto.EntryProto.newBuilder()
                         .setPropertyName(propertyKeyString)
                         .addSnippetMatches(SnippetMatchProto.newBuilder()
-                                .setValuesIndex(0)
-                                .setExactMatchPosition(29)
-                                .setExactMatchBytes(3)
-                                .setWindowPosition(26)
-                                .setWindowBytes(6)
-                                .build())
-                        .build())
-                .build();
-        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(documentProto)
-                .setSnippet(snippetProto)
+                                .setExactMatchBytePosition(29)
+                                .setExactMatchByteLength(3)
+                                .setExactMatchUtf16Position(29)
+                                .setExactMatchUtf16Length(3)
+                                .setWindowBytePosition(26)
+                                .setWindowByteLength(6)
+                                .setWindowUtf16Position(26)
+                                .setWindowUtf16Length(6)))
                 .build();
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(resultProto)
+                .addResults(SearchResultProto.ResultProto.newBuilder()
+                        .setDocument(documentProto)
+                        .setSnippet(snippetProto))
                 .build();
 
         // Making ResultReader and getting Snippet values.
-        SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
-                        Collections.singletonList("packageName"));
-        for (SearchResult result : searchResultPage.getResults()) {
-            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);
-        }
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        SearchResult.MatchInfo match = searchResultPage.getResults().get(0).getMatchInfos().get(0);
+        assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
+        assertThat(match.getFullText()).isEqualTo(propertyValueString);
+        assertThat(match.getExactMatch()).isEqualTo(exactMatch);
+        assertThat(match.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/29, /*upper=*/32));
+        assertThat(match.getFullText()).isEqualTo(propertyValueString);
+        assertThat(match.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/26, /*upper=*/32));
+        assertThat(match.getSnippet()).isEqualTo(window);
     }
 
-    // TODO(tytytyww): Add tests for Double and Long Snippets.
     @Test
-    public void testNoSnippets() throws Exception {
-
+    public void testNoSnippets() {
         final String propertyKeyString = "content";
         final String propertyValueString = "A commonly used fake word is foo.\n"
                 + "   Another nonsense word that’s used a lot\n"
                 + "   is bar.\n";
-        final String uri = "uri1";
-        final String schemaType = "schema1";
-        final String searchWord = "foo";
-        final String exactMatch = "foo";
-        final String window = "is foo";
+        final String id = "id1";
 
         // Building the SearchResult received from query.
-        PropertyProto property = PropertyProto.newBuilder()
-                .setName(propertyKeyString)
-                .addStringValues(propertyValueString)
-                .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri(uri)
-                .setSchema(schemaType)
-                .addProperties(property)
-                .build();
-        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(documentProto)
+                .setUri(id)
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName(propertyKeyString)
+                        .addStringValues(propertyValueString))
                 .build();
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(resultProto)
+                .addResults(SearchResultProto.ResultProto.newBuilder().setDocument(documentProto))
                 .build();
 
-        SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
-                        Collections.singletonList("packageName"));
-        for (SearchResult result : searchResultPage.getResults()) {
-            assertThat(result.getMatches()).isEmpty();
-        }
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getMatchInfos()).isEmpty();
     }
 
     @Test
-    public void testMultipleStringSnippet() throws Exception {
-        final String searchWord = "Test";
-
+    public void testMultipleStringSnippet() {
         // Building the SearchResult received from query.
-        PropertyProto property1 = PropertyProto.newBuilder()
-                .setName("sender.name")
-                .addStringValues("Test Name Jr.")
-                .build();
-        PropertyProto property2 = PropertyProto.newBuilder()
-                .setName("sender.email")
-                .addStringValues("TestNameJr@gmail.com")
-                .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
                 .setUri("uri1")
-                .setSchema("schema1")
-                .addProperties(property1)
-                .addProperties(property2)
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName("senderName")
+                        .addStringValues("Test Name Jr."))
+                .addProperties(PropertyProto.newBuilder()
+                        .setName("senderEmail")
+                        .addStringValues("TestNameJr@gmail.com"))
                 .build();
         SnippetProto snippetProto = SnippetProto.newBuilder()
-                .addEntries(
-                        SnippetProto.EntryProto.newBuilder()
-                                .setPropertyName("sender.name")
-                                .addSnippetMatches(
-                                        SnippetMatchProto.newBuilder()
-                                                .setValuesIndex(0)
-                                                .setExactMatchPosition(0)
-                                                .setExactMatchBytes(4)
-                                                .setWindowPosition(0)
-                                                .setWindowBytes(9)
-                                                .build())
-                                .build())
-                .addEntries(
-                        SnippetProto.EntryProto.newBuilder()
-                                .setPropertyName("sender.email")
-                                .addSnippetMatches(
-                                        SnippetMatchProto.newBuilder()
-                                                .setValuesIndex(0)
-                                                .setExactMatchPosition(0)
-                                                .setExactMatchBytes(20)
-                                                .setWindowPosition(0)
-                                                .setWindowBytes(20)
-                                                .build())
-                                .build()
-                )
-                .build();
-        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(documentProto)
-                .setSnippet(snippetProto)
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("senderName")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchBytePosition(0)
+                                .setExactMatchByteLength(4)
+                                .setExactMatchUtf16Position(0)
+                                .setExactMatchUtf16Length(4)
+                                .setWindowBytePosition(0)
+                                .setWindowByteLength(9)
+                                .setWindowUtf16Position(0)
+                                .setWindowUtf16Length(9)))
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("senderEmail")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchBytePosition(0)
+                                .setExactMatchByteLength(20)
+                                .setExactMatchUtf16Position(0)
+                                .setExactMatchUtf16Length(20)
+                                .setWindowBytePosition(0)
+                                .setWindowByteLength(20)
+                                .setWindowUtf16Position(0)
+                                .setWindowUtf16Length(20)))
                 .build();
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(resultProto)
+                .addResults(SearchResultProto.ResultProto.newBuilder()
+                        .setDocument(documentProto)
+                        .setSnippet(snippetProto))
                 .build();
 
         // Making ResultReader and getting Snippet values.
-        SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
-                        Collections.singletonList("packageName"));
-        for (SearchResult result : searchResultPage.getResults()) {
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
+        assertThat(match1.getPropertyPath()).isEqualTo("senderName");
+        assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
+        assertThat(match1.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4));
+        assertThat(match1.getExactMatch()).isEqualTo("Test");
+        assertThat(match1.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9));
+        assertThat(match1.getSnippet()).isEqualTo("Test Name");
 
-            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");
+        SearchResult.MatchInfo match2 = searchResultPage.getResults().get(0).getMatchInfos().get(1);
+        assertThat(match2.getPropertyPath()).isEqualTo("senderEmail");
+        assertThat(match2.getFullText()).isEqualTo("TestNameJr@gmail.com");
+        assertThat(match2.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
+        assertThat(match2.getExactMatch()).isEqualTo("TestNameJr@gmail.com");
+        assertThat(match2.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
+        assertThat(match2.getSnippet()).isEqualTo("TestNameJr@gmail.com");
+    }
 
-            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");
-        }
+    @Test
+    public void testNestedDocumentSnippet() {
+        // Building the SearchResult received from query.
+        DocumentProto documentProto = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName("sender")
+                        .addDocumentValues(DocumentProto.newBuilder()
+                                .addProperties(PropertyProto.newBuilder()
+                                        .setName("name")
+                                        .addStringValues("Test Name Jr."))
+                                .addProperties(PropertyProto.newBuilder()
+                                        .setName("email")
+                                        .addStringValues("TestNameJr@gmail.com")
+                                        .addStringValues("TestNameJr2@gmail.com"))))
+                .build();
+        SnippetProto snippetProto = SnippetProto.newBuilder()
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("sender.name")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchBytePosition(0)
+                                .setExactMatchByteLength(4)
+                                .setExactMatchUtf16Position(0)
+                                .setExactMatchUtf16Length(4)
+                                .setWindowBytePosition(0)
+                                .setWindowByteLength(9)
+                                .setWindowUtf16Position(0)
+                                .setWindowUtf16Length(9)))
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("sender.email[1]")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchBytePosition(0)
+                                .setExactMatchByteLength(21)
+                                .setExactMatchUtf16Position(0)
+                                .setExactMatchUtf16Length(21)
+                                .setWindowBytePosition(0)
+                                .setWindowByteLength(21)
+                                .setWindowUtf16Position(0)
+                                .setWindowUtf16Length(21)))
+                .build();
+        SearchResultProto searchResultProto = SearchResultProto.newBuilder()
+                .addResults(SearchResultProto.ResultProto.newBuilder()
+                        .setDocument(documentProto)
+                        .setSnippet(snippetProto))
+                .build();
+
+        // Making ResultReader and getting Snippet values.
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
+        assertThat(match1.getPropertyPath()).isEqualTo("sender.name");
+        assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
+        assertThat(match1.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4));
+        assertThat(match1.getExactMatch()).isEqualTo("Test");
+        assertThat(match1.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9));
+        assertThat(match1.getSnippet()).isEqualTo("Test Name");
+
+        SearchResult.MatchInfo match2 = searchResultPage.getResults().get(0).getMatchInfos().get(1);
+        assertThat(match2.getPropertyPath()).isEqualTo("sender.email[1]");
+        assertThat(match2.getFullText()).isEqualTo("TestNameJr2@gmail.com");
+        assertThat(match2.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/21));
+        assertThat(match2.getExactMatch()).isEqualTo("TestNameJr2@gmail.com");
+        assertThat(match2.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/21));
+        assertThat(match2.getSnippet()).isEqualTo("TestNameJr2@gmail.com");
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
new file mode 100644
index 0000000..3d23c0a
--- /dev/null
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchResult;
+
+import org.junit.Test;
+
+public class AppSearchStatsTest {
+    static final String TEST_PACKAGE_NAME = "com.google.test";
+    static final String TEST_DATA_BASE = "testDataBase";
+    static final int TEST_STATUS_CODE = AppSearchResult.RESULT_INTERNAL_ERROR;
+    static final int TEST_TOTAL_LATENCY_MILLIS = 20;
+
+    @Test
+    public void testAppSearchStats_CallStats() {
+        final int estimatedBinderLatencyMillis = 1;
+        final int numOperationsSucceeded = 2;
+        final int numOperationsFailed = 3;
+        final @CallStats.CallType int callType =
+                CallStats.CALL_TYPE_PUT_DOCUMENTS;
+
+        final CallStats cStats = new CallStats.Builder()
+                .setPackageName(TEST_PACKAGE_NAME)
+                .setDatabase(TEST_DATA_BASE)
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .setCallType(callType)
+                .setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
+                .setNumOperationsSucceeded(numOperationsSucceeded)
+                .setNumOperationsFailed(numOperationsFailed)
+                .build();
+
+        assertThat(cStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(cStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(cStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(cStats.getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(cStats.getEstimatedBinderLatencyMillis())
+                .isEqualTo(estimatedBinderLatencyMillis);
+        assertThat(cStats.getCallType()).isEqualTo(callType);
+        assertThat(cStats.getNumOperationsSucceeded()).isEqualTo(numOperationsSucceeded);
+        assertThat(cStats.getNumOperationsFailed()).isEqualTo(numOperationsFailed);
+    }
+
+    @Test
+    public void testAppSearchCallStats_nullValues() {
+        final @CallStats.CallType int callType =
+                CallStats.CALL_TYPE_PUT_DOCUMENTS;
+
+        final CallStats.Builder cStatsBuilder = new CallStats.Builder()
+                .setCallType(callType);
+
+        final CallStats cStats = cStatsBuilder.build();
+
+        assertThat(cStats.getPackageName()).isNull();
+        assertThat(cStats.getDatabase()).isNull();
+        assertThat(cStats.getCallType()).isEqualTo(callType);
+    }
+
+    @Test
+    public void testAppSearchStats_PutDocumentStats() {
+        final int generateDocumentProtoLatencyMillis = 1;
+        final int rewriteDocumentTypesLatencyMillis = 2;
+        final int nativeLatencyMillis = 3;
+        final int nativeDocumentStoreLatencyMillis = 4;
+        final int nativeIndexLatencyMillis = 5;
+        final int nativeIndexMergeLatencyMillis = 6;
+        final int nativeDocumentSize = 7;
+        final int nativeNumTokensIndexed = 8;
+        final boolean nativeExceededMaxNumTokens = true;
+        final PutDocumentStats.Builder pStatsBuilder =
+                new PutDocumentStats.Builder(TEST_PACKAGE_NAME, TEST_DATA_BASE)
+                        .setStatusCode(TEST_STATUS_CODE)
+                        .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                        .setGenerateDocumentProtoLatencyMillis(generateDocumentProtoLatencyMillis)
+                        .setRewriteDocumentTypesLatencyMillis(rewriteDocumentTypesLatencyMillis)
+                        .setNativeLatencyMillis(nativeLatencyMillis)
+                        .setNativeDocumentStoreLatencyMillis(nativeDocumentStoreLatencyMillis)
+                        .setNativeIndexLatencyMillis(nativeIndexLatencyMillis)
+                        .setNativeIndexMergeLatencyMillis(nativeIndexMergeLatencyMillis)
+                        .setNativeDocumentSizeBytes(nativeDocumentSize)
+                        .setNativeNumTokensIndexed(nativeNumTokensIndexed)
+                        .setNativeExceededMaxNumTokens(nativeExceededMaxNumTokens);
+
+        final PutDocumentStats pStats = pStatsBuilder.build();
+
+        assertThat(pStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(pStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(pStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(pStats.getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(pStats.getGenerateDocumentProtoLatencyMillis()).isEqualTo(
+                generateDocumentProtoLatencyMillis);
+        assertThat(pStats.getRewriteDocumentTypesLatencyMillis()).isEqualTo(
+                rewriteDocumentTypesLatencyMillis);
+        assertThat(pStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(pStats.getNativeDocumentStoreLatencyMillis()).isEqualTo(
+                nativeDocumentStoreLatencyMillis);
+        assertThat(pStats.getNativeIndexLatencyMillis()).isEqualTo(nativeIndexLatencyMillis);
+        assertThat(pStats.getNativeIndexMergeLatencyMillis()).isEqualTo(
+                nativeIndexMergeLatencyMillis);
+        assertThat(pStats.getNativeDocumentSizeBytes()).isEqualTo(nativeDocumentSize);
+        assertThat(pStats.getNativeNumTokensIndexed()).isEqualTo(nativeNumTokensIndexed);
+        assertThat(pStats.getNativeExceededMaxNumTokens()).isEqualTo(nativeExceededMaxNumTokens);
+    }
+
+    @Test
+    public void testAppSearchStats_InitializeStats() {
+        int prepareSchemaAndNamespacesLatencyMillis = 1;
+        int prepareVisibilityFileLatencyMillis = 2;
+        int nativeLatencyMillis = 3;
+        int nativeDocumentStoreRecoveryCause = 4;
+        int nativeIndexRestorationCause = 5;
+        int nativeSchemaStoreRecoveryCause = 6;
+        int nativeDocumentStoreRecoveryLatencyMillis = 7;
+        int nativeIndexRestorationLatencyMillis = 8;
+        int nativeSchemaStoreRecoveryLatencyMillis = 9;
+        int nativeDocumentStoreDataStatus = 10;
+        int nativeNumDocuments = 11;
+        int nativeNumSchemaTypes = 12;
+
+        final InitializeStats.Builder iStatsBuilder = new InitializeStats.Builder()
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .setHasDeSync(/* hasDeSyncs= */ true)
+                .setPrepareSchemaAndNamespacesLatencyMillis(prepareSchemaAndNamespacesLatencyMillis)
+                .setPrepareVisibilityStoreLatencyMillis(prepareVisibilityFileLatencyMillis)
+                .setNativeLatencyMillis(nativeLatencyMillis)
+                .setDocumentStoreRecoveryCause(nativeDocumentStoreRecoveryCause)
+                .setIndexRestorationCause(nativeIndexRestorationCause)
+                .setSchemaStoreRecoveryCause(nativeSchemaStoreRecoveryCause)
+                .setDocumentStoreRecoveryLatencyMillis(
+                        nativeDocumentStoreRecoveryLatencyMillis)
+                .setIndexRestorationLatencyMillis(nativeIndexRestorationLatencyMillis)
+                .setSchemaStoreRecoveryLatencyMillis(nativeSchemaStoreRecoveryLatencyMillis)
+                .setDocumentStoreDataStatus(nativeDocumentStoreDataStatus)
+                .setDocumentCount(nativeNumDocuments)
+                .setSchemaTypeCount(nativeNumSchemaTypes)
+                .setHasReset(true)
+                .setResetStatusCode(AppSearchResult.RESULT_INVALID_SCHEMA);
+        final InitializeStats iStats = iStatsBuilder.build();
+
+
+        assertThat(iStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(iStats.getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(iStats.hasDeSync()).isTrue();
+        assertThat(iStats.getPrepareSchemaAndNamespacesLatencyMillis()).isEqualTo(
+                prepareSchemaAndNamespacesLatencyMillis);
+        assertThat(iStats.getPrepareVisibilityStoreLatencyMillis()).isEqualTo(
+                prepareVisibilityFileLatencyMillis);
+        assertThat(iStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(iStats.getDocumentStoreRecoveryCause()).isEqualTo(
+                nativeDocumentStoreRecoveryCause);
+        assertThat(iStats.getIndexRestorationCause()).isEqualTo(nativeIndexRestorationCause);
+        assertThat(iStats.getSchemaStoreRecoveryCause()).isEqualTo(
+                nativeSchemaStoreRecoveryCause);
+        assertThat(iStats.getDocumentStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeDocumentStoreRecoveryLatencyMillis);
+        assertThat(iStats.getIndexRestorationLatencyMillis()).isEqualTo(
+                nativeIndexRestorationLatencyMillis);
+        assertThat(iStats.getSchemaStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeSchemaStoreRecoveryLatencyMillis);
+        assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
+                nativeDocumentStoreDataStatus);
+        assertThat(iStats.getDocumentCount()).isEqualTo(nativeNumDocuments);
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(nativeNumSchemaTypes);
+        assertThat(iStats.hasReset()).isTrue();
+        assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_INVALID_SCHEMA);
+    }
+
+    @Test
+    public void testAppSearchStats_SearchStats() {
+        int rewriteSearchSpecLatencyMillis = 1;
+        int rewriteSearchResultLatencyMillis = 2;
+        int visibilityScope = SearchStats.VISIBILITY_SCOPE_LOCAL;
+        int nativeLatencyMillis = 4;
+        int nativeNumTerms = 5;
+        int nativeQueryLength = 6;
+        int nativeNumNamespacesFiltered = 7;
+        int nativeNumSchemaTypesFiltered = 8;
+        int nativeRequestedPageSize = 9;
+        int nativeNumResultsReturnedCurrentPage = 10;
+        boolean nativeIsFirstPage = true;
+        int nativeParseQueryLatencyMillis = 11;
+        int nativeRankingStrategy = 12;
+        int nativeNumDocumentsScored = 13;
+        int nativeScoringLatencyMillis = 14;
+        int nativeRankingLatencyMillis = 15;
+        int nativeNumResultsSnippeted = 16;
+        int nativeDocumentRetrievingLatencyMillis = 17;
+        final SearchStats.Builder sStatsBuilder = new SearchStats.Builder(visibilityScope,
+                TEST_PACKAGE_NAME)
+                .setDatabase(TEST_DATA_BASE)
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .setRewriteSearchSpecLatencyMillis(rewriteSearchSpecLatencyMillis)
+                .setRewriteSearchResultLatencyMillis(rewriteSearchResultLatencyMillis)
+                .setNativeLatencyMillis(nativeLatencyMillis)
+                .setTermCount(nativeNumTerms)
+                .setQueryLength(nativeQueryLength)
+                .setFilteredNamespaceCount(nativeNumNamespacesFiltered)
+                .setFilteredSchemaTypeCount(nativeNumSchemaTypesFiltered)
+                .setRequestedPageSize(nativeRequestedPageSize)
+                .setCurrentPageReturnedResultCount(nativeNumResultsReturnedCurrentPage)
+                .setIsFirstPage(nativeIsFirstPage)
+                .setParseQueryLatencyMillis(nativeParseQueryLatencyMillis)
+                .setRankingStrategy(nativeRankingStrategy)
+                .setScoredDocumentCount(nativeNumDocumentsScored)
+                .setScoringLatencyMillis(nativeScoringLatencyMillis)
+                .setRankingLatencyMillis(nativeRankingLatencyMillis)
+                .setResultWithSnippetsCount(nativeNumResultsSnippeted)
+                .setDocumentRetrievingLatencyMillis(nativeDocumentRetrievingLatencyMillis);
+        final SearchStats sStats = sStatsBuilder.build();
+
+        assertThat(sStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(sStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(sStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(sStats.getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(sStats.getRewriteSearchSpecLatencyMillis()).isEqualTo(
+                rewriteSearchSpecLatencyMillis);
+        assertThat(sStats.getRewriteSearchResultLatencyMillis()).isEqualTo(
+                rewriteSearchResultLatencyMillis);
+        assertThat(sStats.getVisibilityScope()).isEqualTo(visibilityScope);
+        assertThat(sStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(sStats.getTermCount()).isEqualTo(nativeNumTerms);
+        assertThat(sStats.getQueryLength()).isEqualTo(nativeQueryLength);
+        assertThat(sStats.getFilteredNamespaceCount()).isEqualTo(nativeNumNamespacesFiltered);
+        assertThat(sStats.getFilteredSchemaTypeCount()).isEqualTo(
+                nativeNumSchemaTypesFiltered);
+        assertThat(sStats.getRequestedPageSize()).isEqualTo(nativeRequestedPageSize);
+        assertThat(sStats.getCurrentPageReturnedResultCount()).isEqualTo(
+                nativeNumResultsReturnedCurrentPage);
+        assertThat(sStats.isFirstPage()).isTrue();
+        assertThat(sStats.getParseQueryLatencyMillis()).isEqualTo(
+                nativeParseQueryLatencyMillis);
+        assertThat(sStats.getRankingStrategy()).isEqualTo(nativeRankingStrategy);
+        assertThat(sStats.getScoredDocumentCount()).isEqualTo(nativeNumDocumentsScored);
+        assertThat(sStats.getScoringLatencyMillis()).isEqualTo(nativeScoringLatencyMillis);
+        assertThat(sStats.getRankingLatencyMillis()).isEqualTo(nativeRankingLatencyMillis);
+        assertThat(sStats.getResultWithSnippetsCount()).isEqualTo(nativeNumResultsSnippeted);
+        assertThat(sStats.getDocumentRetrievingLatencyMillis()).isEqualTo(
+                nativeDocumentRetrievingLatencyMillis);
+    }
+
+    @Test
+    public void testAppSearchStats_SetSchemaStats() {
+        SchemaMigrationStats schemaMigrationStats = new SchemaMigrationStats.Builder()
+                .setGetSchemaLatencyMillis(1)
+                .setQueryAndTransformLatencyMillis(2)
+                .setFirstSetSchemaLatencyMillis(3)
+                .setSecondSetSchemaLatencyMillis(4)
+                .setSaveDocumentLatencyMillis(5)
+                .setMigratedDocumentCount(6)
+                .setSavedDocumentCount(7)
+                .build();
+        int newTypeCount = 1;
+        int compatibleTypeChangeCount = 2;
+        int indexIncompatibleTypeChangeCount = 3;
+        int backwardsIncompatibleTypeChangeCount = 4;
+        SetSchemaStats sStats = new SetSchemaStats.Builder(TEST_PACKAGE_NAME, TEST_DATA_BASE)
+                .setStatusCode(TEST_STATUS_CODE)
+                .setSchemaMigrationStats(schemaMigrationStats)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .setNewTypeCount(newTypeCount)
+                .setCompatibleTypeChangeCount(compatibleTypeChangeCount)
+                .setIndexIncompatibleTypeChangeCount(indexIncompatibleTypeChangeCount)
+                .setBackwardsIncompatibleTypeChangeCount(backwardsIncompatibleTypeChangeCount)
+                .build();
+
+        assertThat(sStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(sStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(sStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(sStats.getSchemaMigrationStats()).isEqualTo(schemaMigrationStats);
+        assertThat(sStats.getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(sStats.getNewTypeCount()).isEqualTo(newTypeCount);
+        assertThat(sStats.getCompatibleTypeChangeCount()).isEqualTo(compatibleTypeChangeCount);
+        assertThat(sStats.getIndexIncompatibleTypeChangeCount()).isEqualTo(
+                indexIncompatibleTypeChangeCount);
+        assertThat(sStats.getBackwardsIncompatibleTypeChangeCount()).isEqualTo(
+                backwardsIncompatibleTypeChangeCount);
+    }
+
+    @Test
+    public void testAppSearchStats_SchemaMigrationStats() {
+        int getSchemaLatency = 1;
+        int queryAndTransformLatency = 2;
+        int firstSetSchemaLatency = 3;
+        int secondSetSchemaLatency = 4;
+        int saveDocumentLatency = 5;
+        int migratedDocumentCount = 6;
+        int savedDocumentCount = 7;
+        SchemaMigrationStats sStats = new SchemaMigrationStats.Builder()
+                .setGetSchemaLatencyMillis(getSchemaLatency)
+                .setQueryAndTransformLatencyMillis(queryAndTransformLatency)
+                .setFirstSetSchemaLatencyMillis(firstSetSchemaLatency)
+                .setSecondSetSchemaLatencyMillis(secondSetSchemaLatency)
+                .setSaveDocumentLatencyMillis(saveDocumentLatency)
+                .setMigratedDocumentCount(migratedDocumentCount)
+                .setSavedDocumentCount(savedDocumentCount)
+                .build();
+
+        assertThat(sStats.getGetSchemaLatencyMillis()).isEqualTo(getSchemaLatency);
+        assertThat(sStats.getQueryAndTransformLatencyMillis()).isEqualTo(queryAndTransformLatency);
+        assertThat(sStats.getFirstSetSchemaLatencyMillis()).isEqualTo(firstSetSchemaLatency);
+        assertThat(sStats.getSecondSetSchemaLatencyMillis()).isEqualTo(secondSetSchemaLatency);
+        assertThat(sStats.getSaveDocumentLatencyMillis()).isEqualTo(saveDocumentLatency);
+        assertThat(sStats.getMigratedDocumentCount()).isEqualTo(migratedDocumentCount);
+        assertThat(sStats.getSavedDocumentCount()).isEqualTo(
+                savedDocumentCount);
+    }
+
+    @Test
+    public void testAppSearchStats_RemoveStats() {
+        int nativeLatencyMillis = 1;
+        @RemoveStats.DeleteType int deleteType = 2;
+        int documentDeletedCount = 3;
+
+        final RemoveStats rStats = new RemoveStats.Builder(TEST_PACKAGE_NAME,
+                TEST_DATA_BASE)
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .setNativeLatencyMillis(nativeLatencyMillis)
+                .setDeleteType(deleteType)
+                .setDeletedDocumentCount(documentDeletedCount)
+                .build();
+
+
+        assertThat(rStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(rStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(rStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(rStats.getTotalLatencyMillis()).isEqualTo(TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(rStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(rStats.getDeleteType()).isEqualTo(deleteType);
+        assertThat(rStats.getDeletedDocumentCount()).isEqualTo(documentDeletedCount);
+    }
+
+    @Test
+    public void testAppSearchStats_OptimizeStats() {
+        int nativeLatencyMillis = 1;
+        int nativeDocumentStoreOptimizeLatencyMillis = 2;
+        int nativeIndexRestorationLatencyMillis = 3;
+        int nativeNumOriginalDocuments = 4;
+        int nativeNumDeletedDocuments = 5;
+        int nativeNumExpiredDocuments = 6;
+        long nativeStorageSizeBeforeBytes = Integer.MAX_VALUE + 1;
+        long nativeStorageSizeAfterBytes = Integer.MAX_VALUE + 2;
+        long nativeTimeSinceLastOptimizeMillis = Integer.MAX_VALUE + 3;
+
+        final OptimizeStats oStats = new OptimizeStats.Builder()
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .setNativeLatencyMillis(nativeLatencyMillis)
+                .setDocumentStoreOptimizeLatencyMillis(nativeDocumentStoreOptimizeLatencyMillis)
+                .setIndexRestorationLatencyMillis(nativeIndexRestorationLatencyMillis)
+                .setOriginalDocumentCount(nativeNumOriginalDocuments)
+                .setDeletedDocumentCount(nativeNumDeletedDocuments)
+                .setExpiredDocumentCount(nativeNumExpiredDocuments)
+                .setStorageSizeBeforeBytes(nativeStorageSizeBeforeBytes)
+                .setStorageSizeAfterBytes(nativeStorageSizeAfterBytes)
+                .setTimeSinceLastOptimizeMillis(nativeTimeSinceLastOptimizeMillis)
+                .build();
+
+        assertThat(oStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(oStats.getTotalLatencyMillis()).isEqualTo(TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(oStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(oStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(oStats.getDocumentStoreOptimizeLatencyMillis()).isEqualTo(
+                nativeDocumentStoreOptimizeLatencyMillis);
+        assertThat(oStats.getIndexRestorationLatencyMillis()).isEqualTo(
+                nativeIndexRestorationLatencyMillis);
+        assertThat(oStats.getOriginalDocumentCount()).isEqualTo(nativeNumOriginalDocuments);
+        assertThat(oStats.getDeletedDocumentCount()).isEqualTo(nativeNumDeletedDocuments);
+        assertThat(oStats.getExpiredDocumentCount()).isEqualTo(nativeNumExpiredDocuments);
+        assertThat(oStats.getStorageSizeBeforeBytes()).isEqualTo(nativeStorageSizeBeforeBytes);
+        assertThat(oStats.getStorageSizeAfterBytes()).isEqualTo(nativeStorageSizeAfterBytes);
+        assertThat(oStats.getTimeSinceLastOptimizeMillis()).isEqualTo(
+                nativeTimeSinceLastOptimizeMillis);
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index e97d137..52cd89e 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -16,31 +16,60 @@
 
 package androidx.appsearch.localstorage;
 
+import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
+import androidx.appsearch.localstorage.converter.ResultCodeToProtoConverter;
 import androidx.appsearch.localstorage.converter.SchemaToProtoConverter;
 import androidx.appsearch.localstorage.converter.SearchResultToProtoConverter;
 import androidx.appsearch.localstorage.converter.SearchSpecToProtoConverter;
+import androidx.appsearch.localstorage.converter.SetSchemaResponseToProtoConverter;
+import androidx.appsearch.localstorage.converter.TypePropertyPathToProtoConverter;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.OptimizeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchStats;
+import androidx.appsearch.localstorage.stats.SetSchemaStats;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.util.LogUtil;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
+import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.IcingSearchEngine;
 import com.google.android.icing.proto.DeleteByQueryResultProto;
 import com.google.android.icing.proto.DeleteResultProto;
 import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.DocumentStorageInfoProto;
 import com.google.android.icing.proto.GetAllNamespacesResultProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
 import com.google.android.icing.proto.GetResultProto;
@@ -48,11 +77,13 @@
 import com.google.android.icing.proto.GetSchemaResultProto;
 import com.google.android.icing.proto.IcingSearchEngineOptions;
 import com.google.android.icing.proto.InitializeResultProto;
+import com.google.android.icing.proto.NamespaceStorageInfoProto;
 import com.google.android.icing.proto.OptimizeResultProto;
 import com.google.android.icing.proto.PersistToDiskResultProto;
+import com.google.android.icing.proto.PersistType;
 import com.google.android.icing.proto.PropertyConfigProto;
-import com.google.android.icing.proto.PropertyProto;
 import com.google.android.icing.proto.PutResultProto;
+import com.google.android.icing.proto.ReportUsageResultProto;
 import com.google.android.icing.proto.ResetResultProto;
 import com.google.android.icing.proto.ResultSpecProto;
 import com.google.android.icing.proto.SchemaProto;
@@ -62,12 +93,17 @@
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.SetSchemaResultProto;
 import com.google.android.icing.proto.StatusProto;
+import com.google.android.icing.proto.StorageInfoProto;
+import com.google.android.icing.proto.StorageInfoResultProto;
 import com.google.android.icing.proto.TypePropertyMask;
+import com.google.android.icing.proto.UsageReport;
 
+import java.io.Closeable;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -109,35 +145,27 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @WorkerThread
-public final class AppSearchImpl {
+public final class AppSearchImpl implements Closeable {
     private static final String TAG = "AppSearchImpl";
 
     @VisibleForTesting
-    static final char DATABASE_DELIMITER = '/';
-
-    @VisibleForTesting
-    static final char PACKAGE_DELIMITER = '$';
-
-    @VisibleForTesting
-    static final int OPTIMIZE_THRESHOLD_DOC_COUNT = 1000;
-    @VisibleForTesting
-    static final int OPTIMIZE_THRESHOLD_BYTES = 1_000_000; // 1MB
-    @VisibleForTesting
     static final int CHECK_OPTIMIZE_INTERVAL = 100;
 
     private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
+    private final LogUtil mLogUtil = new LogUtil(TAG);
+    private final OptimizeStrategy mOptimizeStrategy;
+    private final LimitConfig mLimitConfig;
 
     @GuardedBy("mReadWriteLock")
-    private final IcingSearchEngine mIcingSearchEngineLocked;
+    @VisibleForTesting
+    final IcingSearchEngine mIcingSearchEngineLocked;
 
+    // This map contains schema types and SchemaTypeConfigProtos for all package-database
+    // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+    // prefixed schema type to its respective SchemaTypeConfigProto.
     @GuardedBy("mReadWriteLock")
-    private final VisibilityStore mVisibilityStoreLocked;
-
-    // This map contains schemaTypes for all package-database prefixes. All values in the map are
-    // prefixed with the package-database prefix.
-    // TODO(b/172360376): Check if this can be replaced with an ArrayMap
-    @GuardedBy("mReadWriteLock")
-    private final Map<String, Set<String>> mSchemaMapLocked = new HashMap<>();
+    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMapLocked =
+            new ArrayMap<>();
 
     // This map contains namespaces for all package-database prefixes. All values in the map are
     // prefixed with the package-database prefix.
@@ -145,84 +173,202 @@
     @GuardedBy("mReadWriteLock")
     private final Map<String, Set<String>> mNamespaceMapLocked = new HashMap<>();
 
+    /** Maps package name to active document count. */
+    @GuardedBy("mReadWriteLock")
+    private final Map<String, Integer> mDocumentCountMapLocked = new ArrayMap<>();
+
+    // Maps packages to the set of valid nextPageTokens that the package can manipulate. A token
+    // is unique and constant per query (i.e. the same token '123' is used to iterate through
+    // pages of search results). The tokens themselves are generated and tracked by
+    // IcingSearchEngine. IcingSearchEngine considers a token valid and won't be reused
+    // until we call invalidateNextPageToken on the token.
+    //
+    // Note that we synchronize on itself because the nextPageToken cache is checked at
+    // query-time, and queries are done in parallel with a read lock. Ideally, this would be
+    // guarded by the normal mReadWriteLock.writeLock, but ReentrantReadWriteLocks can't upgrade
+    // read to write locks. This lock should be acquired at the smallest scope possible.
+    // mReadWriteLock is a higher-level lock, so calls shouldn't be made out
+    // to any functions that grab the lock.
+    @GuardedBy("mNextPageTokensLocked")
+    private final Map<String, Set<Long>> mNextPageTokensLocked = new ArrayMap<>();
+
     /**
-     * The counter to check when to call {@link #checkForOptimizeLocked(boolean)}. The
+     * The counter to check when to call {@link #checkForOptimize}. The
      * interval is
      * {@link #CHECK_OPTIMIZE_INTERVAL}.
      */
     @GuardedBy("mReadWriteLock")
     private int mOptimizeIntervalCountLocked = 0;
 
+    /** Whether this instance has been closed, and therefore unusable. */
+    @GuardedBy("mReadWriteLock")
+    private boolean mClosedLocked = false;
+
     /**
      * Creates and initializes an instance of {@link AppSearchImpl} which writes data to the given
      * folder.
+     *
+     * <p>Clients can pass a {@link AppSearchLogger} here through their AppSearchSession, but it
+     * can't be saved inside {@link AppSearchImpl}, because the impl will be shared by all the
+     * sessions for the same package in JetPack.
+     *
+     * <p>Instead, logger instance needs to be passed to each individual method, like create, query
+     * and putDocument.
+     *
+     * @param initStatsBuilder collects stats for initialization if provided.
      */
     @NonNull
-    public static AppSearchImpl create(@NonNull File icingDir) throws AppSearchException {
-        Preconditions.checkNotNull(icingDir);
-        AppSearchImpl appSearchImpl = new AppSearchImpl(icingDir);
-        appSearchImpl.initializeVisibilityStore();
-        return appSearchImpl;
+    public static AppSearchImpl create(
+            @NonNull File icingDir,
+            @NonNull LimitConfig limitConfig,
+            @Nullable InitializeStats.Builder initStatsBuilder,
+            @NonNull OptimizeStrategy optimizeStrategy)
+            throws AppSearchException {
+        return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy);
     }
 
-    private AppSearchImpl(@NonNull File icingDir) throws AppSearchException {
-        mReadWriteLock.writeLock().lock();
+    /**
+     * @param initStatsBuilder collects stats for initialization if provided.
+     */
+    private AppSearchImpl(
+            @NonNull File icingDir,
+            @NonNull LimitConfig limitConfig,
+            @Nullable InitializeStats.Builder initStatsBuilder,
+            @NonNull OptimizeStrategy optimizeStrategy)
+            throws AppSearchException {
+        Preconditions.checkNotNull(icingDir);
+        mLimitConfig = Preconditions.checkNotNull(limitConfig);
+        mOptimizeStrategy = Preconditions.checkNotNull(optimizeStrategy);
 
+        mReadWriteLock.writeLock().lock();
         try {
             // We synchronize here because we don't want to call IcingSearchEngine.initialize() more
             // than once. It's unnecessary and can be a costly operation.
             IcingSearchEngineOptions options = IcingSearchEngineOptions.newBuilder()
                     .setBaseDir(icingDir.getAbsolutePath()).build();
+            mLogUtil.piiTrace("Constructing IcingSearchEngine, request", options);
             mIcingSearchEngineLocked = new IcingSearchEngine(options);
+            mLogUtil.piiTrace(
+                    "Constructing IcingSearchEngine, response",
+                    ObjectsCompat.hashCode(mIcingSearchEngineLocked));
 
-            mVisibilityStoreLocked = new VisibilityStore(this);
-
-            InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize();
-            SchemaProto schemaProto;
-            GetAllNamespacesResultProto getAllNamespacesResultProto;
+            // The core initialization procedure. If any part of this fails, we bail into
+            // resetLocked(), deleting all data (but hopefully allowing AppSearchImpl to come up).
             try {
+                mLogUtil.piiTrace("icingSearchEngine.initialize, request");
+                InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize();
+                mLogUtil.piiTrace(
+                        "icingSearchEngine.initialize, response",
+                        initializeResultProto.getStatus(),
+                        initializeResultProto);
+
+                if (initStatsBuilder != null) {
+                    initStatsBuilder
+                            .setStatusCode(
+                                    statusProtoToResultCode(initializeResultProto.getStatus()))
+                            // TODO(b/173532925) how to get DeSyncs value
+                            .setHasDeSync(false);
+                    AppSearchLoggerHelper.copyNativeStats(
+                            initializeResultProto.getInitializeStats(), initStatsBuilder);
+                }
                 checkSuccess(initializeResultProto.getStatus());
-                schemaProto = getSchemaProtoLocked();
-                getAllNamespacesResultProto = mIcingSearchEngineLocked.getAllNamespaces();
+
+                // Read all protos we need to construct AppSearchImpl's cache maps
+                long prepareSchemaAndNamespacesLatencyStartMillis = SystemClock.elapsedRealtime();
+                SchemaProto schemaProto = getSchemaProtoLocked();
+
+                mLogUtil.piiTrace("init:getAllNamespaces, request");
+                GetAllNamespacesResultProto getAllNamespacesResultProto =
+                        mIcingSearchEngineLocked.getAllNamespaces();
+                mLogUtil.piiTrace(
+                        "init:getAllNamespaces, response",
+                        getAllNamespacesResultProto.getNamespacesCount(),
+                        getAllNamespacesResultProto);
+
+                StorageInfoProto storageInfoProto = getRawStorageInfoProto();
+
+                // Log the time it took to read the data that goes into the cache maps
+                if (initStatsBuilder != null) {
+                    // In case there is some error for getAllNamespaces, we can still
+                    // set the latency for preparation.
+                    // If there is no error, the value will be overridden by the actual one later.
+                    initStatsBuilder.setStatusCode(
+                            statusProtoToResultCode(getAllNamespacesResultProto.getStatus()))
+                            .setPrepareSchemaAndNamespacesLatencyMillis(
+                                    (int) (SystemClock.elapsedRealtime()
+                                            - prepareSchemaAndNamespacesLatencyStartMillis));
+                }
                 checkSuccess(getAllNamespacesResultProto.getStatus());
+
+                // Populate schema map
+                List<SchemaTypeConfigProto> schemaProtoTypesList = schemaProto.getTypesList();
+                for (int i = 0; i < schemaProtoTypesList.size(); i++) {
+                    SchemaTypeConfigProto schema = schemaProtoTypesList.get(i);
+                    String prefixedSchemaType = schema.getSchemaType();
+                    addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType), schema);
+                }
+
+                // Populate namespace map
+                List<String> prefixedNamespaceList =
+                        getAllNamespacesResultProto.getNamespacesList();
+                for (int i = 0; i < prefixedNamespaceList.size(); i++) {
+                    String prefixedNamespace = prefixedNamespaceList.get(i);
+                    addToMap(mNamespaceMapLocked, getPrefix(prefixedNamespace), prefixedNamespace);
+                }
+
+                // Populate document count map
+                rebuildDocumentCountMapLocked(storageInfoProto);
+
+                // logging prepare_schema_and_namespaces latency
+                if (initStatsBuilder != null) {
+                    initStatsBuilder.setPrepareSchemaAndNamespacesLatencyMillis(
+                            (int) (SystemClock.elapsedRealtime()
+                                    - prepareSchemaAndNamespacesLatencyStartMillis));
+                }
+
+                mLogUtil.piiTrace("Init completed successfully");
+
             } catch (AppSearchException e) {
-                Log.w(TAG, "Error initializing, resetting IcingSearchEngine.", e);
                 // Some error. Reset and see if it fixes it.
-                reset();
-                return;
+                Log.e(TAG, "Error initializing, resetting IcingSearchEngine.", e);
+                if (initStatsBuilder != null) {
+                    initStatsBuilder.setStatusCode(e.getResultCode());
+                }
+                resetLocked(initStatsBuilder);
             }
 
-            // Populate schema map
-            for (SchemaTypeConfigProto schema : schemaProto.getTypesList()) {
-                String prefixedSchemaType = schema.getSchemaType();
-                addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType),
-                        prefixedSchemaType);
-            }
-
-            // Populate namespace map
-            for (String prefixedNamespace : getAllNamespacesResultProto.getNamespacesList()) {
-                addToMap(mNamespaceMapLocked, getPrefix(prefixedNamespace),
-                        prefixedNamespace);
-            }
-
-            // TODO(b/155939114): It's possible to optimize after init, which would reduce the time
-            //   to when we're able to serve queries. Consider moving this optimize call out.
-            checkForOptimizeLocked(/* force= */ true);
-
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
     }
 
+    @GuardedBy("mReadWriteLock")
+    private void throwIfClosedLocked() {
+        if (mClosedLocked) {
+            throw new IllegalStateException("Trying to use a closed AppSearchImpl instance.");
+        }
+    }
+
     /**
-     * Initialize the visibility store in AppSearchImpl.
+     * Persists data to disk and closes the instance.
      *
-     * @throws AppSearchException on IcingSearchEngine error.
+     * <p>This instance is no longer usable after it's been closed. Call {@link #create} to
+     * create a new, usable instance.
      */
-    void initializeVisibilityStore() throws AppSearchException {
+    @Override
+    public void close() {
         mReadWriteLock.writeLock().lock();
         try {
-            mVisibilityStoreLocked.initialize();
+            if (mClosedLocked) {
+                return;
+            }
+            persistToDisk(PersistType.Code.FULL);
+            mLogUtil.piiTrace("icingSearchEngine.close, request");
+            mIcingSearchEngineLocked.close();
+            mLogUtil.piiTrace("icingSearchEngine.close, response");
+            mClosedLocked = true;
+        } catch (AppSearchException e) {
+            Log.w(TAG, "Error when closing AppSearchImpl.", e);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -236,27 +382,46 @@
      * @param packageName                   The package name that owns the schemas.
      * @param databaseName                  The name of the database where this schema lives.
      * @param schemas                       Schemas to set for this app.
-     * @param schemasNotPlatformSurfaceable Schema types that should not be surfaced on platform
+     * @param visibilityStore               If set, {@code schemasNotDisplayedBySystem} and {@code
+     *                                      schemasVisibleToPackages} will be saved here if the
+     *                                      schema is successfully applied.
+     * @param schemasNotDisplayedBySystem   Schema types that should not be surfaced on platform
      *                                      surfaces.
+     * @param schemasVisibleToPackages      Schema types that are visible to the specified packages.
      * @param forceOverride                 Whether to force-apply the schema even if it is
      *                                      incompatible. Documents
      *                                      which do not comply with the new schema will be deleted.
-     * @throws AppSearchException on IcingSearchEngine error.
+     * @param version                       The overall version number of the request.
+     * @param setSchemaStatsBuilder         Builder for {@link SetSchemaStats} to hold stats for
+     *                                      setSchema
+     * @return The response contains deleted schema types and incompatible schema types of this
+     * call.
+     * @throws AppSearchException On IcingSearchEngine error. If the status code is
+     *                            FAILED_PRECONDITION for the incompatible change, the
+     *                            exception will be converted to the SetSchemaResponse.
      */
-    public void setSchema(
+    @NonNull
+    public SetSchemaResponse setSchema(
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull List<AppSearchSchema> schemas,
-            @NonNull List<String> schemasNotPlatformSurfaceable,
-            boolean forceOverride) throws AppSearchException {
+            @Nullable VisibilityStore visibilityStore,
+            @NonNull List<String> schemasNotDisplayedBySystem,
+            @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages,
+            boolean forceOverride,
+            int version,
+            @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
         mReadWriteLock.writeLock().lock();
         try {
+            throwIfClosedLocked();
+
             SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
 
             SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
             for (int i = 0; i < schemas.size(); i++) {
+                AppSearchSchema schema = schemas.get(i);
                 SchemaTypeConfigProto schemaTypeProto =
-                        SchemaToProtoConverter.toSchemaTypeConfigProto(schemas.get(i));
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version);
                 newSchemaBuilder.addTypes(schemaTypeProto);
             }
 
@@ -268,49 +433,72 @@
                     newSchemaBuilder.build());
 
             // Apply schema
+            SchemaProto finalSchema = existingSchemaBuilder.build();
+            mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
             SetSchemaResultProto setSchemaResultProto =
-                    mIcingSearchEngineLocked.setSchema(existingSchemaBuilder.build(),
-                            forceOverride);
+                    mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride);
+            mLogUtil.piiTrace(
+                    "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto);
+
+            if (setSchemaStatsBuilder != null) {
+                setSchemaStatsBuilder.setStatusCode(statusProtoToResultCode(
+                        setSchemaResultProto.getStatus()));
+                AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto,
+                        setSchemaStatsBuilder);
+            }
 
             // Determine whether it succeeded.
             try {
                 checkSuccess(setSchemaResultProto.getStatus());
             } catch (AppSearchException e) {
-                // Improve the error message by merging in information about incompatible types.
-                if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
-                        || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0) {
-                    String newMessage = e.getMessage()
-                            + "\n  Deleted types: "
-                            + setSchemaResultProto.getDeletedSchemaTypesList()
-                            + "\n  Incompatible types: "
-                            + setSchemaResultProto.getIncompatibleSchemaTypesList();
-                    throw new AppSearchException(e.getResultCode(), newMessage, e.getCause());
+                // Swallow the exception for the incompatible change case. We will propagate
+                // those deleted schemas and incompatible types to the SetSchemaResponse.
+                boolean isFailedPrecondition = setSchemaResultProto.getStatus().getCode()
+                        == StatusProto.Code.FAILED_PRECONDITION;
+                boolean isIncompatible = setSchemaResultProto.getDeletedSchemaTypesCount() > 0
+                        || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0;
+                if (isFailedPrecondition && isIncompatible) {
+                    return SetSchemaResponseToProtoConverter
+                            .toSetSchemaResponse(setSchemaResultProto, prefix);
                 } else {
                     throw e;
                 }
             }
 
             // Update derived data structures.
-            mSchemaMapLocked.put(prefix, rewrittenSchemaResults.mRewrittenPrefixedTypes);
-
-            Set<String> prefixedSchemasNotPlatformSurfaceable =
-                    new ArraySet<>(schemasNotPlatformSurfaceable.size());
-            for (int i = 0; i < schemasNotPlatformSurfaceable.size(); i++) {
-                prefixedSchemasNotPlatformSurfaceable.add(
-                        prefix + schemasNotPlatformSurfaceable.get(i));
+            for (SchemaTypeConfigProto schemaTypeConfigProto :
+                    rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
+                addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
             }
-            mVisibilityStoreLocked.setVisibility(prefix,
-                    prefixedSchemasNotPlatformSurfaceable);
 
-            // Determine whether to schedule an immediate optimize.
-            if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
-                    || (setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0
-                    && forceOverride)) {
-                // Any existing schemas which is not in 'schemas' will be deleted, and all
-                // documents of these types were also deleted. And so well if we force override
-                // incompatible schemas.
-                checkForOptimizeLocked(/* force= */true);
+            for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
+                removeFromMap(mSchemaMapLocked, prefix, schemaType);
             }
+
+            if (visibilityStore != null) {
+                Set<String> prefixedSchemasNotDisplayedBySystem =
+                        new ArraySet<>(schemasNotDisplayedBySystem.size());
+                for (int i = 0; i < schemasNotDisplayedBySystem.size(); i++) {
+                    prefixedSchemasNotDisplayedBySystem.add(
+                            prefix + schemasNotDisplayedBySystem.get(i));
+                }
+
+                Map<String, List<PackageIdentifier>> prefixedSchemasVisibleToPackages =
+                        new ArrayMap<>(schemasVisibleToPackages.size());
+                for (Map.Entry<String, List<PackageIdentifier>> entry
+                        : schemasVisibleToPackages.entrySet()) {
+                    prefixedSchemasVisibleToPackages.put(prefix + entry.getKey(), entry.getValue());
+                }
+
+                visibilityStore.setVisibility(
+                        packageName,
+                        databaseName,
+                        prefixedSchemasNotDisplayedBySystem,
+                        prefixedSchemasVisibleToPackages);
+            }
+
+            return SetSchemaResponseToProtoConverter
+                    .toSetSchemaResponse(setSchemaResultProto, prefix);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -326,47 +514,94 @@
      * @throws AppSearchException on IcingSearchEngine error.
      */
     @NonNull
-    public List<AppSearchSchema> getSchema(@NonNull String packageName,
+    public GetSchemaResponse getSchema(@NonNull String packageName,
             @NonNull String databaseName) throws AppSearchException {
-        SchemaProto fullSchema;
         mReadWriteLock.readLock().lock();
         try {
-            fullSchema = getSchemaProtoLocked();
+            throwIfClosedLocked();
+
+            SchemaProto fullSchema = getSchemaProtoLocked();
+
+            String prefix = createPrefix(packageName, databaseName);
+            GetSchemaResponse.Builder responseBuilder = new GetSchemaResponse.Builder();
+            for (int i = 0; i < fullSchema.getTypesCount(); i++) {
+                String typePrefix = getPrefix(fullSchema.getTypes(i).getSchemaType());
+                if (!prefix.equals(typePrefix)) {
+                    continue;
+                }
+                // Rewrite SchemaProto.types.schema_type
+                SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(
+                        i).toBuilder();
+                String newSchemaType =
+                        typeConfigBuilder.getSchemaType().substring(prefix.length());
+                typeConfigBuilder.setSchemaType(newSchemaType);
+
+                // Rewrite SchemaProto.types.properties.schema_type
+                for (int propertyIdx = 0;
+                        propertyIdx < typeConfigBuilder.getPropertiesCount();
+                        propertyIdx++) {
+                    PropertyConfigProto.Builder propertyConfigBuilder =
+                            typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+                    if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
+                        String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
+                                .substring(prefix.length());
+                        propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+                        typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+                    }
+                }
+
+                AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(
+                        typeConfigBuilder);
+
+                //TODO(b/183050495) find a place to store the version for the database, rather
+                // than read from a schema.
+                responseBuilder.setVersion(fullSchema.getTypes(i).getVersion());
+                responseBuilder.addSchema(schema);
+            }
+            return responseBuilder.build();
         } finally {
             mReadWriteLock.readLock().unlock();
         }
+    }
 
-        String prefix = createPrefix(packageName, databaseName);
-        List<AppSearchSchema> result = new ArrayList<>();
-        for (int i = 0; i < fullSchema.getTypesCount(); i++) {
-            String typePrefix = getPrefix(fullSchema.getTypes(i).getSchemaType());
-            if (!prefix.equals(typePrefix)) {
-                continue;
-            }
-            // Rewrite SchemaProto.types.schema_type
-            SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(i).toBuilder();
-            String newSchemaType =
-                    typeConfigBuilder.getSchemaType().substring(prefix.length());
-            typeConfigBuilder.setSchemaType(newSchemaType);
-
-            // Rewrite SchemaProto.types.properties.schema_type
-            for (int propertyIdx = 0;
-                    propertyIdx < typeConfigBuilder.getPropertiesCount();
-                    propertyIdx++) {
-                PropertyConfigProto.Builder propertyConfigBuilder =
-                        typeConfigBuilder.getProperties(propertyIdx).toBuilder();
-                if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
-                    String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
-                            .substring(prefix.length());
-                    propertyConfigBuilder.setSchemaType(newPropertySchemaType);
-                    typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+    /**
+     * Retrieves the list of namespaces with at least one document for this package name, database.
+     *
+     * <p>This method belongs to query group.
+     *
+     * @param packageName  Package name that owns this schema
+     * @param databaseName The name of the database where this schema lives.
+     * @throws AppSearchException on IcingSearchEngine error.
+     */
+    @NonNull
+    public List<String> getNamespaces(
+            @NonNull String packageName, @NonNull String databaseName) throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+            mLogUtil.piiTrace("getAllNamespaces, request");
+            // We can't just use mNamespaceMap here because we have no way to prune namespaces from
+            // mNamespaceMap when they have no more documents (e.g. after setting schema to empty or
+            // using deleteByQuery).
+            GetAllNamespacesResultProto getAllNamespacesResultProto =
+                    mIcingSearchEngineLocked.getAllNamespaces();
+            mLogUtil.piiTrace(
+                    "getAllNamespaces, response",
+                    getAllNamespacesResultProto.getNamespacesCount(),
+                    getAllNamespacesResultProto);
+            checkSuccess(getAllNamespacesResultProto.getStatus());
+            String prefix = createPrefix(packageName, databaseName);
+            List<String> results = new ArrayList<>();
+            for (int i = 0; i < getAllNamespacesResultProto.getNamespacesCount(); i++) {
+                String prefixedNamespace = getAllNamespacesResultProto.getNamespaces(i);
+                if (prefixedNamespace.startsWith(prefix)) {
+                    results.add(prefixedNamespace.substring(prefix.length()));
                 }
             }
-
-            AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder);
-            result.add(schema);
+            return results;
+        } finally {
+            mReadWriteLock.readLock().unlock();
         }
-        return result;
     }
 
     /**
@@ -380,57 +615,188 @@
      * @throws AppSearchException on IcingSearchEngine error.
      */
     public void putDocument(@NonNull String packageName, @NonNull String databaseName,
-            @NonNull GenericDocument document)
+            @NonNull GenericDocument document, @Nullable AppSearchLogger logger)
             throws AppSearchException {
-        DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto(
-                document).toBuilder();
-        String prefix = createPrefix(packageName, databaseName);
-        addPrefixToDocument(documentBuilder, prefix);
+        PutDocumentStats.Builder pStatsBuilder = null;
+        if (logger != null) {
+            pStatsBuilder = new PutDocumentStats.Builder(packageName, databaseName);
+        }
+        long totalStartTimeMillis = SystemClock.elapsedRealtime();
 
-        PutResultProto putResultProto;
         mReadWriteLock.writeLock().lock();
         try {
-            putResultProto = mIcingSearchEngineLocked.put(documentBuilder.build());
-            addToMap(mNamespaceMapLocked, prefix, documentBuilder.getNamespace());
-            // The existing documents with same URI will be deleted, so there maybe some resources
-            // could be released after optimize().
-            checkForOptimizeLocked(/* force= */ false);
+            throwIfClosedLocked();
+
+            // Generate Document Proto
+            long generateDocumentProtoStartTimeMillis = SystemClock.elapsedRealtime();
+            DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto(
+                    document).toBuilder();
+            long generateDocumentProtoEndTimeMillis = SystemClock.elapsedRealtime();
+
+            // Rewrite Document Type
+            long rewriteDocumentTypeStartTimeMillis = SystemClock.elapsedRealtime();
+            String prefix = createPrefix(packageName, databaseName);
+            addPrefixToDocument(documentBuilder, prefix);
+            long rewriteDocumentTypeEndTimeMillis = SystemClock.elapsedRealtime();
+            DocumentProto finalDocument = documentBuilder.build();
+
+            // Check limits
+            int newDocumentCount = enforceLimitConfigLocked(
+                    packageName, finalDocument.getUri(), finalDocument.getSerializedSize());
+
+            // Insert document
+            mLogUtil.piiTrace("putDocument, request", finalDocument.getUri(), finalDocument);
+            PutResultProto putResultProto = mIcingSearchEngineLocked.put(finalDocument);
+            mLogUtil.piiTrace("putDocument, response", putResultProto.getStatus(), putResultProto);
+
+            // Update caches
+            addToMap(mNamespaceMapLocked, prefix, finalDocument.getNamespace());
+            mDocumentCountMapLocked.put(packageName, newDocumentCount);
+
+            // Logging stats
+            if (pStatsBuilder != null) {
+                pStatsBuilder
+                        .setStatusCode(statusProtoToResultCode(putResultProto.getStatus()))
+                        .setGenerateDocumentProtoLatencyMillis(
+                                (int) (generateDocumentProtoEndTimeMillis
+                                        - generateDocumentProtoStartTimeMillis))
+                        .setRewriteDocumentTypesLatencyMillis(
+                                (int) (rewriteDocumentTypeEndTimeMillis
+                                        - rewriteDocumentTypeStartTimeMillis));
+                AppSearchLoggerHelper.copyNativeStats(putResultProto.getPutDocumentStats(),
+                        pStatsBuilder);
+            }
+
+            checkSuccess(putResultProto.getStatus());
         } finally {
             mReadWriteLock.writeLock().unlock();
+
+            if (logger != null) {
+                long totalEndTimeMillis = SystemClock.elapsedRealtime();
+                pStatsBuilder.setTotalLatencyMillis(
+                        (int) (totalEndTimeMillis - totalStartTimeMillis));
+                logger.logStats(pStatsBuilder.build());
+            }
         }
-        checkSuccess(putResultProto.getStatus());
     }
 
     /**
-     * Retrieves a document from the AppSearch index by URI.
+     * Checks that a new document can be added to the given packageName with the given serialized
+     * size without violating our {@link LimitConfig}.
+     *
+     * @return the new count of documents for the given package, including the new document.
+     * @throws AppSearchException with a code of {@link AppSearchResult#RESULT_OUT_OF_SPACE} if the
+     *                            limits are violated by the new document.
+     */
+    @GuardedBy("mReadWriteLock")
+    private int enforceLimitConfigLocked(String packageName, String newDocUri, int newDocSize)
+            throws AppSearchException {
+        // Limits check: size of document
+        if (newDocSize > mLimitConfig.getMaxDocumentSizeBytes()) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_OUT_OF_SPACE,
+                    "Document \"" + newDocUri + "\" for package \"" + packageName
+                            + "\" serialized to " + newDocSize + " bytes, which exceeds "
+                            + "limit of " + mLimitConfig.getMaxDocumentSizeBytes() + " bytes");
+        }
+
+        // Limits check: number of documents
+        Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName);
+        int newDocumentCount;
+        if (oldDocumentCount == null) {
+            newDocumentCount = 1;
+        } else {
+            newDocumentCount = oldDocumentCount + 1;
+        }
+        if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) {
+            // Our management of mDocumentCountMapLocked doesn't account for document
+            // replacements, so our counter might have overcounted if the app has replaced docs.
+            // Rebuild the counter from StorageInfo in case this is so.
+            // TODO(b/170371356):  If Icing lib exposes something in the result which says
+            //  whether the document was a replacement, we could subtract 1 again after the put
+            //  to keep the count accurate. That would allow us to remove this code.
+            rebuildDocumentCountMapLocked(getRawStorageInfoProto());
+            oldDocumentCount = mDocumentCountMapLocked.get(packageName);
+            if (oldDocumentCount == null) {
+                newDocumentCount = 1;
+            } else {
+                newDocumentCount = oldDocumentCount + 1;
+            }
+        }
+        if (newDocumentCount > mLimitConfig.getMaxDocumentCount()) {
+            // Now we really can't fit it in, even accounting for replacements.
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_OUT_OF_SPACE,
+                    "Package \"" + packageName + "\" exceeded limit of "
+                            + mLimitConfig.getMaxDocumentCount() + " documents. Some documents "
+                            + "must be removed to index additional ones.");
+        }
+
+        return newDocumentCount;
+    }
+
+    /**
+     * Retrieves a document from the AppSearch index by namespace and document ID.
      *
      * <p>This method belongs to query group.
      *
-     * @param packageName  The package that owns this document.
-     * @param databaseName The databaseName this document resides in.
-     * @param namespace    The namespace this document resides in.
-     * @param uri          The URI of the document to get.
+     * @param packageName       The package that owns this document.
+     * @param databaseName      The databaseName this document resides in.
+     * @param namespace         The namespace this document resides in.
+     * @param id                The ID of the document to get.
+     * @param typePropertyPaths A map of schema type to a list of property paths to return in the
+     *                          result.
      * @return The Document contents
      * @throws AppSearchException on IcingSearchEngine error.
      */
     @NonNull
-    public GenericDocument getDocument(@NonNull String packageName, @NonNull String databaseName,
+    public GenericDocument getDocument(
+            @NonNull String packageName, @NonNull String databaseName,
             @NonNull String namespace,
-            @NonNull String uri) throws AppSearchException {
-        GetResultProto getResultProto;
+            @NonNull String id,
+            @NonNull Map<String, List<String>> typePropertyPaths) throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
-            getResultProto = mIcingSearchEngineLocked.get(
-                    createPrefix(packageName, databaseName) + namespace, uri,
-                    GetResultSpecProto.getDefaultInstance());
+            throwIfClosedLocked();
+            String prefix = createPrefix(packageName, databaseName);
+            List<TypePropertyMask> nonPrefixedPropertyMasks =
+                    TypePropertyPathToProtoConverter.toTypePropertyMaskList(typePropertyPaths);
+            List<TypePropertyMask> prefixedPropertyMasks =
+                    new ArrayList<>(nonPrefixedPropertyMasks.size());
+            for (int i = 0; i < nonPrefixedPropertyMasks.size(); ++i) {
+                TypePropertyMask typePropertyMask = nonPrefixedPropertyMasks.get(i);
+                String nonPrefixedType = typePropertyMask.getSchemaType();
+                String prefixedType = nonPrefixedType.equals(
+                        GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
+                        ? nonPrefixedType : prefix + nonPrefixedType;
+                prefixedPropertyMasks.add(
+                        typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
+            }
+            GetResultSpecProto getResultSpec =
+                    GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks
+                    ).build();
+
+            String finalNamespace = createPrefix(packageName, databaseName) + namespace;
+            if (mLogUtil.isPiiTraceEnabled()) {
+                mLogUtil.piiTrace(
+                        "getDocument, request", finalNamespace + ", " + id + "," + getResultSpec);
+            }
+            GetResultProto getResultProto =
+                    mIcingSearchEngineLocked.get(finalNamespace, id, getResultSpec);
+            mLogUtil.piiTrace("getDocument, response", getResultProto.getStatus(), getResultProto);
+            checkSuccess(getResultProto.getStatus());
+
+            // The schema type map cannot be null at this point. It could only be null if no
+            // schema had ever been set for that prefix. Given we have retrieved a document from
+            // the index, we know a schema had to have been set.
+            Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMapLocked.get(prefix);
+            DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
+            removePrefixesFromDocument(documentBuilder);
+            return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
+                    prefix, schemaTypeMap);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
-        checkSuccess(getResultProto.getStatus());
-
-        DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
-        removePrefixesFromDocument(documentBuilder);
-        return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build());
     }
 
     /**
@@ -442,6 +808,7 @@
      * @param databaseName    The databaseName this query for.
      * @param queryExpression Query String to search.
      * @param searchSpec      Spec for setting filters, raw query etc.
+     * @param logger          logger to collect query stats
      * @return The results of performing this search. It may contain an empty list of results if
      * no documents matched the query.
      * @throws AppSearchException on IcingSearchEngine error.
@@ -451,14 +818,48 @@
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull String queryExpression,
-            @NonNull SearchSpec searchSpec) throws AppSearchException {
+            @NonNull SearchSpec searchSpec,
+            @Nullable AppSearchLogger logger) throws AppSearchException {
+        long totalLatencyStartMillis = SystemClock.elapsedRealtime();
+        SearchStats.Builder sStatsBuilder = null;
+        if (logger != null) {
+            sStatsBuilder =
+                    new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL,
+                            packageName).setDatabase(databaseName);
+        }
+
         mReadWriteLock.readLock().lock();
         try {
-            return doQueryLocked(Collections.singleton(createPrefix(packageName, databaseName)),
-                    queryExpression,
-                    searchSpec);
+            throwIfClosedLocked();
+
+            List<String> filterPackageNames = searchSpec.getFilterPackageNames();
+            if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) {
+                // Client wanted to query over some packages that weren't its own. This isn't
+                // allowed through local query so we can return early with no results.
+                if (logger != null) {
+                    sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR);
+                }
+                return new SearchResultPage(Bundle.EMPTY);
+            }
+
+            String prefix = createPrefix(packageName, databaseName);
+            Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
+
+            SearchResultPage searchResultPage =
+                    doQueryLocked(Collections.singleton(createPrefix(packageName, databaseName)),
+                            allowedPrefixedSchemas,
+                            queryExpression,
+                            searchSpec,
+                            sStatsBuilder);
+            addNextPageToken(packageName, searchResultPage.getNextPageToken());
+            return searchResultPage;
         } finally {
             mReadWriteLock.readLock().unlock();
+            if (logger != null) {
+                sStatsBuilder.setTotalLatencyMillis(
+                        (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
+                logger.logStats(sStatsBuilder.build());
+            }
         }
     }
 
@@ -468,8 +869,16 @@
      *
      * <p>This method belongs to query group.
      *
-     * @param queryExpression Query String to search.
-     * @param searchSpec      Spec for setting filters, raw query etc.
+     * @param queryExpression       Query String to search.
+     * @param searchSpec            Spec for setting filters, raw query etc.
+     * @param callerPackageName     Package name of the caller, should belong to the {@code
+     *                              callerUserHandle}.
+     * @param visibilityStore       Optional visibility store to obtain system and package
+     *                              visibility settings from
+     * @param callerUid             UID of the client making the globalQuery call.
+     * @param callerHasSystemAccess Whether the caller has been positively identified as having
+     *                              access to schemas marked system surfaceable.
+     * @param logger                logger to collect globalQuery stats
      * @return The results of performing this search. It may contain an empty list of results if
      * no documents matched the query.
      * @throws AppSearchException on IcingSearchEngine error.
@@ -477,21 +886,129 @@
     @NonNull
     public SearchResultPage globalQuery(
             @NonNull String queryExpression,
-            @NonNull SearchSpec searchSpec) throws AppSearchException {
-        // TODO(b/169883602): Check if the platform is querying us at a higher level. At this
-        //  point, we should add all platform-surfaceable schemas assuming the querier has been
-        //  verified.
+            @NonNull SearchSpec searchSpec,
+            @NonNull String callerPackageName,
+            @Nullable VisibilityStore visibilityStore,
+            int callerUid,
+            boolean callerHasSystemAccess,
+            @Nullable AppSearchLogger logger) throws AppSearchException {
+        long totalLatencyStartMillis = SystemClock.elapsedRealtime();
+        SearchStats.Builder sStatsBuilder = null;
+        if (logger != null) {
+            sStatsBuilder =
+                    new SearchStats.Builder(
+                            SearchStats.VISIBILITY_SCOPE_GLOBAL,
+                            callerPackageName);
+        }
+
         mReadWriteLock.readLock().lock();
         try {
-            // We use the mNamespaceMap.keySet here because it's the smaller set of valid prefixes
-            // that could exist.
-            Set<String> prefixes = mNamespaceMapLocked.keySet();
+            throwIfClosedLocked();
 
-            // Filter out any VisibilityStore documents which are AppSearch-internal only.
-            prefixes.remove(createPrefix(VisibilityStore.PACKAGE_NAME,
-                    VisibilityStore.DATABASE_NAME));
+            // Convert package filters to prefix filters
+            Set<String> packageFilters = new ArraySet<>(searchSpec.getFilterPackageNames());
+            Set<String> prefixFilters = new ArraySet<>();
+            if (packageFilters.isEmpty()) {
+                // Client didn't restrict their search over packages. Try to query over all
+                // packages/prefixes
+                prefixFilters = mNamespaceMapLocked.keySet();
+            } else {
+                // Client did restrict their search over packages. Only include the prefixes that
+                // belong to the specified packages.
+                for (String prefix : mNamespaceMapLocked.keySet()) {
+                    String packageName = getPackageName(prefix);
+                    if (packageFilters.contains(packageName)) {
+                        prefixFilters.add(prefix);
+                    }
+                }
+            }
 
-            return doQueryLocked(prefixes, queryExpression, searchSpec);
+            // Convert schema filters to prefixed schema filters
+            ArraySet<String> prefixedSchemaFilters = new ArraySet<>();
+            for (String prefix : prefixFilters) {
+                List<String> schemaFilters = searchSpec.getFilterSchemas();
+                if (schemaFilters.isEmpty()) {
+                    // Client didn't specify certain schemas to search over, check all schemas
+                    prefixedSchemaFilters.addAll(mSchemaMapLocked.get(prefix).keySet());
+                } else {
+                    // Client specified some schemas to search over, check each one
+                    for (int i = 0; i < schemaFilters.size(); i++) {
+                        prefixedSchemaFilters.add(prefix + schemaFilters.get(i));
+                    }
+                }
+            }
+
+            // Remove the schemas the client is not allowed to search over
+            Iterator<String> prefixedSchemaIt = prefixedSchemaFilters.iterator();
+            while (prefixedSchemaIt.hasNext()) {
+                String prefixedSchema = prefixedSchemaIt.next();
+                String packageName = getPackageName(prefixedSchema);
+
+                boolean allow;
+                if (packageName.equals(callerPackageName)) {
+                    // Callers can always retrieve their own data
+                    allow = true;
+                } else if (visibilityStore == null) {
+                    // If there's no visibility store, there's no extra access
+                    allow = false;
+                } else {
+                    String databaseName = getDatabaseName(prefixedSchema);
+                    allow = visibilityStore.isSchemaSearchableByCaller(
+                            packageName,
+                            databaseName,
+                            prefixedSchema,
+                            callerUid,
+                            callerHasSystemAccess);
+                }
+
+                if (!allow) {
+                    prefixedSchemaIt.remove();
+                }
+            }
+
+            SearchResultPage searchResultPage = doQueryLocked(
+                    prefixFilters,
+                    prefixedSchemaFilters,
+                    queryExpression,
+                    searchSpec,
+                    sStatsBuilder);
+            addNextPageToken(callerPackageName, searchResultPage.getNextPageToken());
+            return searchResultPage;
+        } finally {
+            mReadWriteLock.readLock().unlock();
+
+            if (logger != null) {
+                sStatsBuilder.setTotalLatencyMillis(
+                        (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
+                logger.logStats(sStatsBuilder.build());
+            }
+        }
+    }
+
+    /**
+     * Returns a mapping of package names to all the databases owned by that package.
+     *
+     * <p>This method is inefficient to call repeatedly.
+     */
+    @NonNull
+    public Map<String, Set<String>> getPackageToDatabases() {
+        mReadWriteLock.readLock().lock();
+        try {
+            Map<String, Set<String>> packageToDatabases = new ArrayMap<>();
+            for (String prefix : mSchemaMapLocked.keySet()) {
+                String packageName = getPackageName(prefix);
+
+                Set<String> databases = packageToDatabases.get(packageName);
+                if (databases == null) {
+                    databases = new ArraySet<>();
+                    packageToDatabases.put(packageName, databases);
+                }
+
+                String databaseName = getDatabaseName(prefix);
+                databases.add(databaseName);
+            }
+
+            return packageToDatabases;
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -499,35 +1016,85 @@
 
     @GuardedBy("mReadWriteLock")
     private SearchResultPage doQueryLocked(
-            @NonNull Set<String> prefixes, @NonNull String queryExpression,
-            @NonNull SearchSpec searchSpec)
+            @NonNull Set<String> prefixes,
+            @NonNull Set<String> allowedPrefixedSchemas,
+            @NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec,
+            @Nullable SearchStats.Builder sStatsBuilder)
             throws AppSearchException {
+        long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
+
         SearchSpecProto.Builder searchSpecBuilder =
                 SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder().setQuery(
                         queryExpression);
-        // rewriteSearchSpecForPrefixesLocked will return false if none of the prefixes that the
-        // client is trying to search on exist, so we can return an empty SearchResult and skip
+        // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
+        // over given their search filters, so we can return an empty SearchResult and skip
         // sending request to Icing.
-        if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes)) {
+        if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes,
+                allowedPrefixedSchemas)) {
+            if (sStatsBuilder != null) {
+                sStatsBuilder.setRewriteSearchSpecLatencyMillis(
+                        (int) (SystemClock.elapsedRealtime()
+                                - rewriteSearchSpecLatencyStartMillis));
+            }
             return new SearchResultPage(Bundle.EMPTY);
         }
 
+        // rewriteSearchSpec, rewriteResultSpec and convertScoringSpec are all counted in
+        // rewriteSearchSpecLatencyMillis
         ResultSpecProto.Builder resultSpecBuilder =
                 SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder();
 
-        // rewriteResultSpecForPrefixesLocked will return false if none of the prefixes that the
-        // client is trying to search on exist, so we can return an empty SearchResult and skip
-        // sending request to Icing.
-        if (!rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes)) {
-            return new SearchResultPage(Bundle.EMPTY);
+        int groupingType = searchSpec.getResultGroupingTypeFlags();
+        if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0
+                && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+            addPerPackagePerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
+                    searchSpec.getResultGroupingLimit());
+        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) {
+            addPerPackageResultGroupingsLocked(resultSpecBuilder, prefixes,
+                    searchSpec.getResultGroupingLimit());
+        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+            addPerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
+                    searchSpec.getResultGroupingLimit());
         }
 
+        rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes, allowedPrefixedSchemas);
         ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
+        SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
+        ResultSpecProto finalResultSpec = resultSpecBuilder.build();
+
+        long rewriteSearchSpecLatencyEndMillis = SystemClock.elapsedRealtime();
+
+        if (mLogUtil.isPiiTraceEnabled()) {
+            mLogUtil.piiTrace(
+                    "search, request",
+                    finalSearchSpec.getQuery(),
+                    finalSearchSpec + ", " + scoringSpec + ", " + finalResultSpec);
+        }
         SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
-                searchSpecBuilder.build(), scoringSpec, resultSpecBuilder.build());
+                finalSearchSpec, scoringSpec, finalResultSpec);
+        mLogUtil.piiTrace(
+                "search, response", searchResultProto.getResultsCount(), searchResultProto);
+
+        if (sStatsBuilder != null) {
+            sStatsBuilder
+                    .setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()))
+                    .setRewriteSearchSpecLatencyMillis((int) (rewriteSearchSpecLatencyEndMillis
+                            - rewriteSearchSpecLatencyStartMillis));
+            AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder);
+        }
+
         checkSuccess(searchResultProto.getStatus());
 
-        return rewriteSearchResultProto(searchResultProto);
+        long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
+        SearchResultPage resultPage = rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
+        if (sStatsBuilder != null) {
+            sStatsBuilder.setRewriteSearchResultLatencyMillis(
+                    (int) (SystemClock.elapsedRealtime()
+                            - rewriteSearchResultLatencyStartMillis));
+        }
+
+        return resultPage;
     }
 
     /**
@@ -536,19 +1103,28 @@
      *
      * <p>This method belongs to query group.
      *
+     * @param packageName   Package name of the caller.
      * @param nextPageToken The token of pre-loaded results of previously executed query.
      * @return The next page of results of previously executed query.
-     * @throws AppSearchException on IcingSearchEngine error.
+     * @throws AppSearchException on IcingSearchEngine error or if can't advance on nextPageToken.
      */
     @NonNull
-    public SearchResultPage getNextPage(long nextPageToken)
+    public SearchResultPage getNextPage(@NonNull String packageName, long nextPageToken)
             throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
+            throwIfClosedLocked();
+
+            mLogUtil.piiTrace("getNextPage, request", nextPageToken);
+            checkNextPageToken(packageName, nextPageToken);
             SearchResultProto searchResultProto = mIcingSearchEngineLocked.getNextPage(
                     nextPageToken);
+            mLogUtil.piiTrace(
+                    "getNextPage, response",
+                    searchResultProto.getResultsCount(),
+                    searchResultProto);
             checkSuccess(searchResultProto.getStatus());
-            return rewriteSearchResultProto(searchResultProto);
+            return rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -559,42 +1135,111 @@
      *
      * <p>This method belongs to query group.
      *
+     * @param packageName   Package name of the caller.
      * @param nextPageToken The token of pre-loaded results of previously executed query to be
      *                      Invalidated.
+     * @throws AppSearchException if nextPageToken is unusable.
      */
-    public void invalidateNextPageToken(long nextPageToken) {
+    public void invalidateNextPageToken(@NonNull String packageName, long nextPageToken)
+            throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
+            throwIfClosedLocked();
+
+            mLogUtil.piiTrace("invalidateNextPageToken, request", nextPageToken);
+            checkNextPageToken(packageName, nextPageToken);
             mIcingSearchEngineLocked.invalidateNextPageToken(nextPageToken);
+
+            synchronized (mNextPageTokensLocked) {
+                // At this point, we're guaranteed that this nextPageToken exists for this package,
+                // otherwise checkNextPageToken would've thrown an exception.
+                mNextPageTokensLocked.get(packageName).remove(nextPageToken);
+            }
         } finally {
             mReadWriteLock.readLock().unlock();
         }
     }
 
-    /**
-     * Removes the given document by URI.
-     *
-     * <p>This method belongs to mutate group.
-     *
-     * @param packageName  The package name that owns the document.
-     * @param databaseName The databaseName the document is in.
-     * @param namespace    Namespace of the document to remove.
-     * @param uri          URI of the document to remove.
-     * @throws AppSearchException on IcingSearchEngine error.
-     */
-    public void remove(@NonNull String packageName, @NonNull String databaseName,
+    /** Reports a usage of the given document at the given timestamp. */
+    public void reportUsage(
+            @NonNull String packageName,
+            @NonNull String databaseName,
             @NonNull String namespace,
-            @NonNull String uri) throws AppSearchException {
-        String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
-        DeleteResultProto deleteResultProto;
+            @NonNull String documentId,
+            long usageTimestampMillis,
+            boolean systemUsage) throws AppSearchException {
         mReadWriteLock.writeLock().lock();
         try {
-            deleteResultProto = mIcingSearchEngineLocked.delete(prefixedNamespace, uri);
-            checkForOptimizeLocked(/* force= */false);
+            throwIfClosedLocked();
+
+            String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
+            UsageReport.UsageType usageType = systemUsage
+                    ? UsageReport.UsageType.USAGE_TYPE2 : UsageReport.UsageType.USAGE_TYPE1;
+            UsageReport report = UsageReport.newBuilder()
+                    .setDocumentNamespace(prefixedNamespace)
+                    .setDocumentUri(documentId)
+                    .setUsageTimestampMs(usageTimestampMillis)
+                    .setUsageType(usageType)
+                    .build();
+
+            mLogUtil.piiTrace("reportUsage, request", report.getDocumentUri(), report);
+            ReportUsageResultProto result = mIcingSearchEngineLocked.reportUsage(report);
+            mLogUtil.piiTrace("reportUsage, response", result.getStatus(), result);
+            checkSuccess(result.getStatus());
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
-        checkSuccess(deleteResultProto.getStatus());
+    }
+
+    /**
+     * Removes the given document by id.
+     *
+     * <p>This method belongs to mutate group.
+     *
+     * @param packageName        The package name that owns the document.
+     * @param databaseName       The databaseName the document is in.
+     * @param namespace          Namespace of the document to remove.
+     * @param id                 ID of the document to remove.
+     * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove
+     * @throws AppSearchException on IcingSearchEngine error.
+     */
+    public void remove(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull String namespace,
+            @NonNull String id,
+            @Nullable RemoveStats.Builder removeStatsBuilder) throws AppSearchException {
+        long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
+        mReadWriteLock.writeLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
+            if (mLogUtil.isPiiTraceEnabled()) {
+                mLogUtil.piiTrace("removeById, request", prefixedNamespace + ", " + id);
+            }
+            DeleteResultProto deleteResultProto =
+                    mIcingSearchEngineLocked.delete(prefixedNamespace, id);
+            mLogUtil.piiTrace(
+                    "removeById, response", deleteResultProto.getStatus(), deleteResultProto);
+
+            if (removeStatsBuilder != null) {
+                removeStatsBuilder.setStatusCode(statusProtoToResultCode(
+                        deleteResultProto.getStatus()));
+                AppSearchLoggerHelper.copyNativeStats(deleteResultProto.getDeleteStats(),
+                        removeStatsBuilder);
+            }
+            checkSuccess(deleteResultProto.getStatus());
+
+            // Update derived maps
+            updateDocumentCountAfterRemovalLocked(packageName, /*numDocumentsDeleted=*/ 1);
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+            if (removeStatsBuilder != null) {
+                removeStatsBuilder.setTotalLatencyMillis(
+                        (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis));
+            }
+        }
     }
 
     /**
@@ -602,95 +1247,423 @@
      *
      * <p>This method belongs to mutate group.
      *
-     * @param packageName     The package name that owns the documents.
-     * @param databaseName    The databaseName the document is in.
-     * @param queryExpression Query String to search.
-     * @param searchSpec      Defines what and how to remove
+     * @param packageName        The package name that owns the documents.
+     * @param databaseName       The databaseName the document is in.
+     * @param queryExpression    Query String to search.
+     * @param searchSpec         Defines what and how to remove
+     * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove
      * @throws AppSearchException on IcingSearchEngine error.
      */
     public void removeByQuery(@NonNull String packageName, @NonNull String databaseName,
             @NonNull String queryExpression,
-            @NonNull SearchSpec searchSpec)
+            @NonNull SearchSpec searchSpec,
+            @Nullable RemoveStats.Builder removeStatsBuilder)
             throws AppSearchException {
-        SearchSpecProto searchSpecProto =
-                SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
-        SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
-                .setQuery(queryExpression);
-        DeleteByQueryResultProto deleteResultProto;
+        long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
         mReadWriteLock.writeLock().lock();
         try {
-            // Only rewrite SearchSpec for non empty prefixes.
-            // rewriteSearchSpecForPrefixesLocked will return false for empty prefixes, we
-            // should skip sending request to Icing and return in here.
-            if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder,
-                    Collections.singleton(createPrefix(packageName, databaseName)))) {
+            throwIfClosedLocked();
+
+            List<String> filterPackageNames = searchSpec.getFilterPackageNames();
+            if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) {
+                // We're only removing documents within the parameter `packageName`. If we're not
+                // restricting our remove-query to this package name, then there's nothing for us to
+                // remove.
                 return;
             }
-            deleteResultProto = mIcingSearchEngineLocked.deleteByQuery(
-                    searchSpecBuilder.build());
-            checkForOptimizeLocked(/* force= */true);
+
+            SearchSpecProto searchSpecProto =
+                    SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
+            SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
+                    .setQuery(queryExpression);
+
+            String prefix = createPrefix(packageName, databaseName);
+            Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
+
+            // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
+            // over given their search filters, so we can return early and skip sending request
+            // to Icing.
+            if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder,
+                    Collections.singleton(prefix), allowedPrefixedSchemas)) {
+                return;
+            }
+            SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
+            mLogUtil.piiTrace("removeByQuery, request", finalSearchSpec);
+            DeleteByQueryResultProto deleteResultProto = mIcingSearchEngineLocked.deleteByQuery(
+                    finalSearchSpec);
+            mLogUtil.piiTrace(
+                    "removeByQuery, response", deleteResultProto.getStatus(), deleteResultProto);
+
+            if (removeStatsBuilder != null) {
+                removeStatsBuilder.setStatusCode(statusProtoToResultCode(
+                        deleteResultProto.getStatus()));
+                // TODO(b/187206766) also log query stats here once IcingLib returns it
+                AppSearchLoggerHelper.copyNativeStats(deleteResultProto.getDeleteByQueryStats(),
+                        removeStatsBuilder);
+            }
+
+            // It seems that the caller wants to get success if the data matching the query is
+            // not in the DB because it was not there or was successfully deleted.
+            checkCodeOneOf(deleteResultProto.getStatus(),
+                    StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
+
+            // Update derived maps
+            int numDocumentsDeleted =
+                    deleteResultProto.getDeleteByQueryStats().getNumDocumentsDeleted();
+            updateDocumentCountAfterRemovalLocked(packageName, numDocumentsDeleted);
         } finally {
             mReadWriteLock.writeLock().unlock();
+            if (removeStatsBuilder != null) {
+                removeStatsBuilder.setTotalLatencyMillis(
+                        (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis));
+            }
         }
-        // It seems that the caller wants to get success if the data matching the query is not in
-        // the DB because it was not there or was successfully deleted.
-        checkCodeOneOf(deleteResultProto.getStatus(),
-                StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
+    }
+
+    @GuardedBy("mReadWriteLock")
+    private void updateDocumentCountAfterRemovalLocked(
+            @NonNull String packageName, int numDocumentsDeleted) {
+        if (numDocumentsDeleted > 0) {
+            Integer oldDocumentCount = mDocumentCountMapLocked.get(packageName);
+            // This should always be true: how can we delete documents for a package without
+            // having seen that package during init? This is just a safeguard.
+            if (oldDocumentCount != null) {
+                // This should always be >0; how can we remove more documents than we've indexed?
+                // This is just a safeguard.
+                int newDocumentCount = Math.max(oldDocumentCount - numDocumentsDeleted, 0);
+                mDocumentCountMapLocked.put(packageName, newDocumentCount);
+            }
+        }
+    }
+
+    /** Estimates the storage usage info for a specific package. */
+    @NonNull
+    public StorageInfo getStorageInfoForPackage(@NonNull String packageName)
+            throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
+            Set<String> databases = packageToDatabases.get(packageName);
+            if (databases == null) {
+                // Package doesn't exist, no storage info to report
+                return new StorageInfo.Builder().build();
+            }
+
+            // Accumulate all the namespaces we're interested in.
+            Set<String> wantedPrefixedNamespaces = new ArraySet<>();
+            for (String database : databases) {
+                Set<String> prefixedNamespaces = mNamespaceMapLocked.get(createPrefix(packageName,
+                        database));
+                if (prefixedNamespaces != null) {
+                    wantedPrefixedNamespaces.addAll(prefixedNamespaces);
+                }
+            }
+            if (wantedPrefixedNamespaces.isEmpty()) {
+                return new StorageInfo.Builder().build();
+            }
+
+            return getStorageInfoForNamespaces(getRawStorageInfoProto(),
+                    wantedPrefixedNamespaces);
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    /** Estimates the storage usage info for a specific database in a package. */
+    @NonNull
+    public StorageInfo getStorageInfoForDatabase(@NonNull String packageName,
+            @NonNull String databaseName)
+            throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
+            Set<String> databases = packageToDatabases.get(packageName);
+            if (databases == null) {
+                // Package doesn't exist, no storage info to report
+                return new StorageInfo.Builder().build();
+            }
+            if (!databases.contains(databaseName)) {
+                // Database doesn't exist, no storage info to report
+                return new StorageInfo.Builder().build();
+            }
+
+            Set<String> wantedPrefixedNamespaces =
+                    mNamespaceMapLocked.get(createPrefix(packageName, databaseName));
+            if (wantedPrefixedNamespaces == null || wantedPrefixedNamespaces.isEmpty()) {
+                return new StorageInfo.Builder().build();
+            }
+
+            return getStorageInfoForNamespaces(getRawStorageInfoProto(),
+                    wantedPrefixedNamespaces);
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Returns the native storage info capsuled in {@link StorageInfoResultProto} directly from
+     * IcingSearchEngine.
+     */
+    @NonNull
+    public StorageInfoProto getRawStorageInfoProto() throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+            mLogUtil.piiTrace("getStorageInfo, request");
+            StorageInfoResultProto storageInfoResult = mIcingSearchEngineLocked.getStorageInfo();
+            mLogUtil.piiTrace("getStorageInfo, response", storageInfoResult.getStatus(),
+                    storageInfoResult);
+            checkSuccess(storageInfoResult.getStatus());
+            return storageInfoResult.getStorageInfo();
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Extracts and returns {@link StorageInfo} from {@link StorageInfoProto} based on
+     * prefixed namespaces.
+     */
+    @NonNull
+    private static StorageInfo getStorageInfoForNamespaces(
+            @NonNull StorageInfoProto storageInfoProto,
+            @NonNull Set<String> prefixedNamespaces) {
+        if (!storageInfoProto.hasDocumentStorageInfo()) {
+            return new StorageInfo.Builder().build();
+        }
+
+        long totalStorageSize = storageInfoProto.getTotalStorageSize();
+        DocumentStorageInfoProto documentStorageInfo =
+                storageInfoProto.getDocumentStorageInfo();
+        int totalDocuments =
+                documentStorageInfo.getNumAliveDocuments()
+                        + documentStorageInfo.getNumExpiredDocuments();
+
+        if (totalStorageSize == 0 || totalDocuments == 0) {
+            // Maybe we can exit early and also avoid a divide by 0 error.
+            return new StorageInfo.Builder().build();
+        }
+
+        // Accumulate stats across the package's namespaces.
+        int aliveDocuments = 0;
+        int expiredDocuments = 0;
+        int aliveNamespaces = 0;
+        List<NamespaceStorageInfoProto> namespaceStorageInfos =
+                documentStorageInfo.getNamespaceStorageInfoList();
+        for (int i = 0; i < namespaceStorageInfos.size(); i++) {
+            NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfos.get(i);
+            // The namespace from icing lib is already the prefixed format
+            if (prefixedNamespaces.contains(namespaceStorageInfo.getNamespace())) {
+                if (namespaceStorageInfo.getNumAliveDocuments() > 0) {
+                    aliveNamespaces++;
+                    aliveDocuments += namespaceStorageInfo.getNumAliveDocuments();
+                }
+                expiredDocuments += namespaceStorageInfo.getNumExpiredDocuments();
+            }
+        }
+        int namespaceDocuments = aliveDocuments + expiredDocuments;
+
+        // Since we don't have the exact size of all the documents, we do an estimation. Note
+        // that while the total storage takes into account schema, index, etc. in addition to
+        // documents, we'll only calculate the percentage based on number of documents a
+        // client has.
+        return new StorageInfo.Builder()
+                .setSizeBytes((long) (namespaceDocuments * 1.0 / totalDocuments * totalStorageSize))
+                .setAliveDocumentsCount(aliveDocuments)
+                .setAliveNamespacesCount(aliveNamespaces)
+                .build();
     }
 
     /**
      * Persists all update/delete requests to the disk.
      *
-     * <p>If the app crashes after a call to PersistToDisk(), Icing would be able to fully recover
-     * all data written up to this point without a costly recovery process.
+     * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#FULL}, Icing
+     * would be able to fully recover all data written up to this point without a costly recovery
+     * process.
      *
-     * <p>If the app crashes before a call to PersistToDisk(), Icing would trigger a costly
-     * recovery process in next initialization. After that, Icing would still be able to recover
-     * all written data.
+     * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#LITE}, Icing
+     * would trigger a costly recovery process in next initialization. After that, Icing would still
+     * be able to recover all written data - excepting Usage data. Usage data is only guaranteed
+     * to be safe after a call to PersistToDisk with {@link PersistType.Code#FULL}
+     *
+     * <p>If the app crashes after an update/delete request has been made, but before any call to
+     * PersistToDisk, then all data in Icing will be lost.
+     *
+     * @param persistType the amount of data to persist. {@link PersistType.Code#LITE} will only
+     *                    persist the minimal amount of data to ensure all data can be recovered.
+     *                    {@link PersistType.Code#FULL} will persist all data necessary to
+     *                    prevent data loss without needing data recovery.
+     * @throws AppSearchException on any error that AppSearch persist data to disk.
      */
-    public void persistToDisk() throws AppSearchException {
-        PersistToDiskResultProto persistToDiskResultProto =
-                mIcingSearchEngineLocked.persistToDisk();
-        checkSuccess(persistToDiskResultProto.getStatus());
+    public void persistToDisk(@NonNull PersistType.Code persistType) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            mLogUtil.piiTrace("persistToDisk, request", persistType);
+            PersistToDiskResultProto persistToDiskResultProto =
+                    mIcingSearchEngineLocked.persistToDisk(persistType);
+            mLogUtil.piiTrace(
+                    "persistToDisk, response",
+                    persistToDiskResultProto.getStatus(),
+                    persistToDiskResultProto);
+            checkSuccess(persistToDiskResultProto.getStatus());
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
     }
 
+    /**
+     * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s under the given package.
+     *
+     * @param packageName The name of package to be removed.
+     * @throws AppSearchException if we cannot remove the data.
+     */
+    public void clearPackageData(@NonNull String packageName) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            throwIfClosedLocked();
+            Set<String> existingPackages = getPackageToDatabases().keySet();
+            if (existingPackages.contains(packageName)) {
+                existingPackages.remove(packageName);
+                prunePackageData(existingPackages);
+            }
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s that doesn't belong to any
+     * of the given installed packages
+     *
+     * @param installedPackages The name of all installed package.
+     * @throws AppSearchException if we cannot remove the data.
+     */
+    public void prunePackageData(@NonNull Set<String> installedPackages) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            throwIfClosedLocked();
+            Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
+            if (installedPackages.containsAll(packageToDatabases.keySet())) {
+                // No package got removed. We are good.
+                return;
+            }
+
+            // Prune schema proto
+            SchemaProto existingSchema = getSchemaProtoLocked();
+            SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
+            for (int i = 0; i < existingSchema.getTypesCount(); i++) {
+                String packageName = getPackageName(existingSchema.getTypes(i).getSchemaType());
+                if (installedPackages.contains(packageName)) {
+                    newSchemaBuilder.addTypes(existingSchema.getTypes(i));
+                }
+            }
+
+            SchemaProto finalSchema = newSchemaBuilder.build();
+
+            // Apply schema, set force override to true to remove all schemas and documents that
+            // doesn't belong to any of these installed packages.
+            mLogUtil.piiTrace(
+                    "clearPackageData.setSchema, request",
+                    finalSchema.getTypesCount(),
+                    finalSchema);
+            SetSchemaResultProto setSchemaResultProto = mIcingSearchEngineLocked.setSchema(
+                    finalSchema, /*ignoreErrorsAndDeleteDocuments=*/ true);
+            mLogUtil.piiTrace(
+                    "clearPackageData.setSchema, response",
+                    setSchemaResultProto.getStatus(),
+                    setSchemaResultProto);
+
+            // Determine whether it succeeded.
+            checkSuccess(setSchemaResultProto.getStatus());
+
+            // Prune cached maps
+            for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) {
+                String packageName = entry.getKey();
+                Set<String> databaseNames = entry.getValue();
+                if (!installedPackages.contains(packageName) && databaseNames != null) {
+                    mDocumentCountMapLocked.remove(packageName);
+                    synchronized (mNextPageTokensLocked) {
+                        mNextPageTokensLocked.remove(packageName);
+                    }
+                    for (String databaseName : databaseNames) {
+                        String removedPrefix = createPrefix(packageName, databaseName);
+                        mSchemaMapLocked.remove(removedPrefix);
+                        mNamespaceMapLocked.remove(removedPrefix);
+                    }
+                }
+            }
+            //TODO(b/145759910) clear visibility setting for package.
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+    }
 
     /**
      * Clears documents and schema across all packages and databaseNames.
      *
-     * <p>This method also clear all data in {@link VisibilityStore}, an
-     * {@link #initializeVisibilityStore()} must be called after this.
-     *
      * <p>This method belongs to mutate group.
      *
      * @throws AppSearchException on IcingSearchEngine error.
      */
-    private void reset() throws AppSearchException {
-        ResetResultProto resetResultProto;
-        mReadWriteLock.writeLock().lock();
-        try {
-            resetResultProto = mIcingSearchEngineLocked.reset();
-            mOptimizeIntervalCountLocked = 0;
-            mSchemaMapLocked.clear();
-            mNamespaceMapLocked.clear();
-
-            // Must be called after everything else since VisibilityStore may repopulate
-            // IcingSearchEngine with an initial schema.
-            mVisibilityStoreLocked.handleReset();
-        } finally {
-            mReadWriteLock.writeLock().unlock();
+    @GuardedBy("mReadWriteLock")
+    private void resetLocked(@Nullable InitializeStats.Builder initStatsBuilder)
+            throws AppSearchException {
+        mLogUtil.piiTrace("icingSearchEngine.reset, request");
+        ResetResultProto resetResultProto = mIcingSearchEngineLocked.reset();
+        mLogUtil.piiTrace(
+                "icingSearchEngine.reset, response",
+                resetResultProto.getStatus(),
+                resetResultProto);
+        mOptimizeIntervalCountLocked = 0;
+        mSchemaMapLocked.clear();
+        mNamespaceMapLocked.clear();
+        mDocumentCountMapLocked.clear();
+        synchronized (mNextPageTokensLocked) {
+            mNextPageTokensLocked.clear();
         }
+        if (initStatsBuilder != null) {
+            initStatsBuilder
+                    .setHasReset(true)
+                    .setResetStatusCode(statusProtoToResultCode(resetResultProto.getStatus()));
+        }
+
         checkSuccess(resetResultProto.getStatus());
     }
 
+    @GuardedBy("mReadWriteLock")
+    private void rebuildDocumentCountMapLocked(@NonNull StorageInfoProto storageInfoProto) {
+        mDocumentCountMapLocked.clear();
+        List<NamespaceStorageInfoProto> namespaceStorageInfoProtoList =
+                storageInfoProto.getDocumentStorageInfo().getNamespaceStorageInfoList();
+        for (int i = 0; i < namespaceStorageInfoProtoList.size(); i++) {
+            NamespaceStorageInfoProto namespaceStorageInfoProto =
+                    namespaceStorageInfoProtoList.get(i);
+            String packageName = getPackageName(namespaceStorageInfoProto.getNamespace());
+            Integer oldCount = mDocumentCountMapLocked.get(packageName);
+            int newCount;
+            if (oldCount == null) {
+                newCount = namespaceStorageInfoProto.getNumAliveDocuments();
+            } else {
+                newCount = oldCount + namespaceStorageInfoProto.getNumAliveDocuments();
+            }
+            mDocumentCountMapLocked.put(packageName, newCount);
+        }
+    }
+
     /** Wrapper around schema changes */
     @VisibleForTesting
     static class RewrittenSchemaResults {
         // Any prefixed types that used to exist in the schema, but are deleted in the new one.
         final Set<String> mDeletedPrefixedTypes = new ArraySet<>();
 
-        // Prefixed types that were part of the new schema.
-        final Set<String> mRewrittenPrefixedTypes = new ArraySet<>();
+        // Map of prefixed schema types to SchemaTypeConfigProtos that were part of the new schema.
+        final Map<String, SchemaTypeConfigProto> mRewrittenPrefixedTypes = new ArrayMap<>();
     }
 
     /**
@@ -738,7 +1711,7 @@
 
         // newTypesToProto is modified below, so we need a copy first
         RewrittenSchemaResults rewrittenSchemaResults = new RewrittenSchemaResults();
-        rewrittenSchemaResults.mRewrittenPrefixedTypes.addAll(newTypesToProto.keySet());
+        rewrittenSchemaResults.mRewrittenPrefixedTypes.putAll(newTypesToProto);
 
         // Combine the existing schema (which may have types from other prefixes) with this
         // prefix's new schema. Modifies the existingSchemaBuilder.
@@ -764,107 +1737,24 @@
     }
 
     /**
-     * Prepends {@code prefix} to all types and namespaces mentioned anywhere in
-     * {@code documentBuilder}.
+     * Rewrites the search spec filters with {@code prefixes}.
      *
-     * @param documentBuilder The document to mutate
-     * @param prefix          The prefix to add
-     */
-    @VisibleForTesting
-    static void addPrefixToDocument(
-            @NonNull DocumentProto.Builder documentBuilder,
-            @NonNull String prefix) {
-        // Rewrite the type name to include/remove the prefix.
-        String newSchema = prefix + documentBuilder.getSchema();
-        documentBuilder.setSchema(newSchema);
-
-        // Rewrite the namespace to include/remove the prefix.
-        documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
-
-        // Recurse into derived documents
-        for (int propertyIdx = 0;
-                propertyIdx < documentBuilder.getPropertiesCount();
-                propertyIdx++) {
-            int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
-            if (documentCount > 0) {
-                PropertyProto.Builder propertyBuilder =
-                        documentBuilder.getProperties(propertyIdx).toBuilder();
-                for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
-                    DocumentProto.Builder derivedDocumentBuilder =
-                            propertyBuilder.getDocumentValues(documentIdx).toBuilder();
-                    addPrefixToDocument(derivedDocumentBuilder, prefix);
-                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
-                }
-                documentBuilder.setProperties(propertyIdx, propertyBuilder);
-            }
-        }
-    }
-
-    /**
-     * Removes any prefixes from types and namespaces mentioned anywhere in
-     * {@code documentBuilder}.
-     *
-     * @param documentBuilder The document to mutate
-     * @return Prefix name that was removed from the document.
-     * @throws AppSearchException if there are unexpected database prefixing errors.
-     */
-    @NonNull
-    @VisibleForTesting
-    static String removePrefixesFromDocument(@NonNull DocumentProto.Builder documentBuilder)
-            throws AppSearchException {
-        // Rewrite the type name and namespace to remove the prefix.
-        String schemaPrefix = getPrefix(documentBuilder.getSchema());
-        String namespacePrefix = getPrefix(documentBuilder.getNamespace());
-
-        if (!schemaPrefix.equals(namespacePrefix)) {
-            throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, "Found unexpected"
-                    + " multiple prefix names in document: " + schemaPrefix + ", "
-                    + namespacePrefix);
-        }
-
-        documentBuilder.setSchema(removePrefix(documentBuilder.getSchema()));
-        documentBuilder.setNamespace(removePrefix(documentBuilder.getNamespace()));
-
-        // Recurse into derived documents
-        for (int propertyIdx = 0;
-                propertyIdx < documentBuilder.getPropertiesCount();
-                propertyIdx++) {
-            int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
-            if (documentCount > 0) {
-                PropertyProto.Builder propertyBuilder =
-                        documentBuilder.getProperties(propertyIdx).toBuilder();
-                for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
-                    DocumentProto.Builder derivedDocumentBuilder =
-                            propertyBuilder.getDocumentValues(documentIdx).toBuilder();
-                    String nestedPrefix = removePrefixesFromDocument(derivedDocumentBuilder);
-                    if (!nestedPrefix.equals(schemaPrefix)) {
-                        throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
-                                "Found unexpected multiple prefix names in document: "
-                                        + schemaPrefix + ", " + nestedPrefix);
-                    }
-                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
-                }
-                documentBuilder.setProperties(propertyIdx, propertyBuilder);
-            }
-        }
-
-        return schemaPrefix;
-    }
-
-    /**
-     * Rewrites the schemaTypeFilters and namespacesFilters that exist with {@code prefixes}.
-     *
-     * <p>If the searchSpec has empty filter lists, all prefixes filters will be added.
      * <p>This method should be only called in query methods and get the READ lock to keep thread
      * safety.
      *
-     * @return false if none of the requested prefixes exist.
+     * @param searchSpecBuilder      Client-provided SearchSpec
+     * @param prefixes               Prefixes that we should prepend to all our filters
+     * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over. This
+     *                               supersedes the schema filters that may exist on the {@code
+     *                               searchSpecBuilder}.
+     * @return false if none there would be nothing to search over.
      */
     @VisibleForTesting
     @GuardedBy("mReadWriteLock")
     boolean rewriteSearchSpecForPrefixesLocked(
             @NonNull SearchSpecProto.Builder searchSpecBuilder,
-            @NonNull Set<String> prefixes) {
+            @NonNull Set<String> prefixes,
+            @NonNull Set<String> allowedPrefixedSchemas) {
         // Create a copy since retainAll() modifies the original set.
         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
         existingPrefixes.retainAll(prefixes);
@@ -874,39 +1764,40 @@
             return false;
         }
 
-        // Cache the schema type filters and namespaces before clearing everything.
-        List<String> schemaTypeFilters = searchSpecBuilder.getSchemaTypeFiltersList();
-        searchSpecBuilder.clearSchemaTypeFilters();
+        if (allowedPrefixedSchemas.isEmpty()) {
+            // Not allowed to search over any schemas, empty query.
+            return false;
+        }
 
+        // Clear the schema type filters since we'll be rewriting them with the
+        // allowedPrefixedSchemas.
+        searchSpecBuilder.clearSchemaTypeFilters();
+        searchSpecBuilder.addAllSchemaTypeFilters(allowedPrefixedSchemas);
+
+        // Cache the namespaces before clearing everything.
         List<String> namespaceFilters = searchSpecBuilder.getNamespaceFiltersList();
         searchSpecBuilder.clearNamespaceFilters();
 
-        // Rewrite filters to include a prefix.
+        // Rewrite non-schema filters to include a prefix.
         for (String prefix : existingPrefixes) {
-            Set<String> existingSchemaTypes = mSchemaMapLocked.get(prefix);
-            if (schemaTypeFilters.isEmpty()) {
-                // Include all schema types
-                searchSpecBuilder.addAllSchemaTypeFilters(existingSchemaTypes);
-            } else {
-                // Add the prefix to the given schema types
-                for (int i = 0; i < schemaTypeFilters.size(); i++) {
-                    String prefixedType = prefix + schemaTypeFilters.get(i);
-                    if (existingSchemaTypes.contains(prefixedType)) {
-                        searchSpecBuilder.addSchemaTypeFilters(prefixedType);
-                    }
-                }
-            }
+            // TODO(b/169883602): We currently grab every namespace for every prefix. We can
+            //  optimize this by checking if a prefix has any allowedSchemaTypes. If not, that
+            //  means we don't want to query over anything in that prefix anyways, so we don't
+            //  need to grab its namespaces either.
 
+            // Empty namespaces on the search spec means to query over all namespaces.
             Set<String> existingNamespaces = mNamespaceMapLocked.get(prefix);
-            if (namespaceFilters.isEmpty()) {
-                // Include all namespaces
-                searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
-            } else {
-                // Prefix the given namespaces.
-                for (int i = 0; i < namespaceFilters.size(); i++) {
-                    String prefixedNamespace = prefix + namespaceFilters.get(i);
-                    if (existingNamespaces.contains(prefixedNamespace)) {
-                        searchSpecBuilder.addNamespaceFilters(prefixedNamespace);
+            if (existingNamespaces != null) {
+                if (namespaceFilters.isEmpty()) {
+                    // Include all namespaces
+                    searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
+                } else {
+                    // Prefix the given namespaces.
+                    for (int i = 0; i < namespaceFilters.size(); i++) {
+                        String prefixedNamespace = prefix + namespaceFilters.get(i);
+                        if (existingNamespaces.contains(prefixedNamespace)) {
+                            searchSpecBuilder.addNamespaceFilters(prefixedNamespace);
+                        }
                     }
                 }
             }
@@ -916,31 +1807,55 @@
     }
 
     /**
+     * Returns the set of allowed prefixed schemas that the {@code prefix} can query while taking
+     * into account the {@code searchSpec} schema filters.
+     *
+     * <p>This only checks intersection of schema filters on the search spec with those that the
+     * prefix owns itself. This does not check global query permissions.
+     */
+    @GuardedBy("mReadWriteLock")
+    private Set<String> getAllowedPrefixSchemasLocked(@NonNull String prefix,
+            @NonNull SearchSpec searchSpec) {
+        Set<String> allowedPrefixedSchemas = new ArraySet<>();
+
+        // Add all the schema filters the client specified.
+        List<String> schemaFilters = searchSpec.getFilterSchemas();
+        for (int i = 0; i < schemaFilters.size(); i++) {
+            allowedPrefixedSchemas.add(prefix + schemaFilters.get(i));
+        }
+
+        if (allowedPrefixedSchemas.isEmpty()) {
+            // If the client didn't specify any schema filters, search over all of their schemas
+            Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMapLocked.get(prefix);
+            if (prefixedSchemaMap != null) {
+                allowedPrefixedSchemas.addAll(prefixedSchemaMap.keySet());
+            }
+        }
+        return allowedPrefixedSchemas;
+    }
+
+    /**
      * Rewrites the typePropertyMasks that exist in {@code prefixes}.
      *
      * <p>This method should be only called in query methods and get the READ lock to keep thread
      * safety.
      *
-     * @return false if none of the requested prefixes exist.
+     * @param resultSpecBuilder      ResultSpecs as specified by client
+     * @param prefixes               Prefixes that we should prepend to all our filters
+     * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over.
      */
     @VisibleForTesting
     @GuardedBy("mReadWriteLock")
-    boolean rewriteResultSpecForPrefixesLocked(
+    void rewriteResultSpecForPrefixesLocked(
             @NonNull ResultSpecProto.Builder resultSpecBuilder,
-            @NonNull Set<String> prefixes) {
+            @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas) {
         // Create a copy since retainAll() modifies the original set.
         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
         existingPrefixes.retainAll(prefixes);
 
-        if (existingPrefixes.isEmpty()) {
-            // None of the prefixes exist, empty query.
-            return false;
-        }
-
         List<TypePropertyMask> prefixedTypePropertyMasks = new ArrayList<>();
         // Rewrite filters to include a database prefix.
         for (String prefix : existingPrefixes) {
-            Set<String> existingSchemaTypes = mSchemaMapLocked.get(prefix);
             // Qualify the given schema types
             for (TypePropertyMask typePropertyMask :
                     resultSpecBuilder.getTypePropertyMasksList()) {
@@ -948,7 +1863,7 @@
                 boolean isWildcard =
                         unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
                 String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
-                if (isWildcard || existingSchemaTypes.contains(prefixedType)) {
+                if (isWildcard || allowedPrefixedSchemas.contains(prefixedType)) {
                     prefixedTypePropertyMasks.add(
                             typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
                 }
@@ -956,94 +1871,192 @@
         }
         resultSpecBuilder.clearTypePropertyMasks().addAllTypePropertyMasks(
                 prefixedTypePropertyMasks);
-        return true;
+    }
+
+    /**
+     * Adds result groupings for each namespace in each package being queried for.
+     *
+     * <p>This method should be only called in query methods and get the READ lock to keep thread
+     * safety.
+     *
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     */
+    @GuardedBy("mReadWriteLock")
+    private void addPerPackagePerNamespaceResultGroupingsLocked(
+            @NonNull ResultSpecProto.Builder resultSpecBuilder,
+            @NonNull Set<String> prefixes, int maxNumResults) {
+        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
+        existingPrefixes.retainAll(prefixes);
+
+        // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
+        // same as the list of namespaces. If one package has multiple databases, each with the same
+        // namespace, then those should be grouped together.
+        Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
+        for (String prefix : existingPrefixes) {
+            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
+            if (prefixedNamespaces == null) {
+                continue;
+            }
+            String packageName = getPackageName(prefix);
+            // Create a new prefix without the database name. This will allow us to group namespaces
+            // that have the same name and package but a different database name together.
+            String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
+            for (String prefixedNamespace : prefixedNamespaces) {
+                String namespace;
+                try {
+                    namespace = removePrefix(prefixedNamespace);
+                } catch (AppSearchException e) {
+                    // This should never happen. Skip this namespace if it does.
+                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+                    continue;
+                }
+                String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
+                List<String> namespaceList =
+                        packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
+                if (namespaceList == null) {
+                    namespaceList = new ArrayList<>();
+                    packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace,
+                            namespaceList);
+                }
+                namespaceList.add(prefixedNamespace);
+            }
+        }
+
+        for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+        }
+    }
+
+    /**
+     * Adds result groupings for each package being queried for.
+     *
+     * <p>This method should be only called in query methods and get the READ lock to keep thread
+     * safety.
+     *
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     */
+    @GuardedBy("mReadWriteLock")
+    private void addPerPackageResultGroupingsLocked(
+            @NonNull ResultSpecProto.Builder resultSpecBuilder,
+            @NonNull Set<String> prefixes, int maxNumResults) {
+        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
+        existingPrefixes.retainAll(prefixes);
+
+        // Build up a map of package to namespaces.
+        Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
+        for (String prefix : existingPrefixes) {
+            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
+            if (prefixedNamespaces == null) {
+                continue;
+            }
+            String packageName = getPackageName(prefix);
+            List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
+            if (packageNamespaceList == null) {
+                packageNamespaceList = new ArrayList<>();
+                packageToNamespacesMap.put(packageName, packageNamespaceList);
+            }
+            packageNamespaceList.addAll(prefixedNamespaces);
+        }
+
+        for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(prefixedNamespaces).setMaxResults(maxNumResults));
+        }
+    }
+
+    /**
+     * Adds result groupings for each namespace being queried for.
+     *
+     * <p>This method should be only called in query methods and get the READ lock to keep thread
+     * safety.
+     *
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     */
+    @GuardedBy("mReadWriteLock")
+    private void addPerNamespaceResultGroupingsLocked(
+            @NonNull ResultSpecProto.Builder resultSpecBuilder,
+            @NonNull Set<String> prefixes, int maxNumResults) {
+        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
+        existingPrefixes.retainAll(prefixes);
+
+        // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the
+        // same as the list of namespaces. If a namespace exists under different packages and/or
+        // different databases, they should still be grouped together.
+        Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
+        for (String prefix : existingPrefixes) {
+            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
+            if (prefixedNamespaces == null) {
+                continue;
+            }
+            for (String prefixedNamespace : prefixedNamespaces) {
+                String namespace;
+                try {
+                    namespace = removePrefix(prefixedNamespace);
+                } catch (AppSearchException e) {
+                    // This should never happen. Skip this namespace if it does.
+                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+                    continue;
+                }
+                List<String> groupedPrefixedNamespaces =
+                        namespaceToPrefixedNamespaces.get(namespace);
+                if (groupedPrefixedNamespaces == null) {
+                    groupedPrefixedNamespaces = new ArrayList<>();
+                    namespaceToPrefixedNamespaces.put(namespace,
+                            groupedPrefixedNamespaces);
+                }
+                groupedPrefixedNamespaces.add(prefixedNamespace);
+            }
+        }
+
+        for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+        }
     }
 
     @VisibleForTesting
     @GuardedBy("mReadWriteLock")
     SchemaProto getSchemaProtoLocked() throws AppSearchException {
+        mLogUtil.piiTrace("getSchema, request");
         GetSchemaResultProto schemaProto = mIcingSearchEngineLocked.getSchema();
+        mLogUtil.piiTrace("getSchema, response", schemaProto.getStatus(), schemaProto);
         // TODO(b/161935693) check GetSchemaResultProto is success or not. Call reset() if it's not.
         // TODO(b/161935693) only allow GetSchemaResultProto NOT_FOUND on first run
         checkCodeOneOf(schemaProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
         return schemaProto.getSchema();
     }
 
-    /**
-     * Returns true if the {@code packageName} and {@code databaseName} has the
-     * {@code schemaType}
-     */
-    @GuardedBy("mReadWriteLock")
-    boolean hasSchemaTypeLocked(@NonNull String packageName, @NonNull String databaseName,
-            @NonNull String schemaType) {
-        Preconditions.checkNotNull(packageName);
-        Preconditions.checkNotNull(databaseName);
-        Preconditions.checkNotNull(schemaType);
-
-        String prefix = createPrefix(packageName, databaseName);
-        Set<String> schemaTypes = mSchemaMapLocked.get(prefix);
-        if (schemaTypes == null) {
-            return false;
+    private void addNextPageToken(String packageName, long nextPageToken) {
+        synchronized (mNextPageTokensLocked) {
+            Set<Long> tokens = mNextPageTokensLocked.get(packageName);
+            if (tokens == null) {
+                tokens = new ArraySet<>();
+                mNextPageTokensLocked.put(packageName, tokens);
+            }
+            tokens.add(nextPageToken);
         }
-
-        return schemaTypes.contains(prefix + schemaType);
     }
 
-    /** Returns a set of all prefixes AppSearchImpl knows about. */
-    @GuardedBy("mReadWriteLock")
-    @NonNull
-    Set<String> getPrefixesLocked() {
-        return mSchemaMapLocked.keySet();
-    }
-
-    @NonNull
-    static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
-        return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
-    }
-
-    /**
-     * Returns the package name that's contained within the {@code prefix}.
-     *
-     * @param prefix Prefix string that contains the package name inside of it. The package name
-     *               must be in the front of the string, and separated from the rest of the
-     *               string by the {@link #PACKAGE_DELIMITER}.
-     * @return Valid package name.
-     */
-    @NonNull
-    private static String getPackageName(@NonNull String prefix) {
-        int delimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
-        if (delimiterIndex == -1) {
-            // This should never happen if we construct our prefixes properly
-            Log.wtf(TAG, "Malformed prefix doesn't contain package name: " + prefix);
-            return "";
-        }
-        return prefix.substring(0, delimiterIndex);
-    }
-
-    @NonNull
-    private static String removePrefix(@NonNull String prefixedString)
+    private void checkNextPageToken(String packageName, long nextPageToken)
             throws AppSearchException {
-        // The prefix is made up of the package, then the database. So we only need to find the
-        // database cutoff.
-        int delimiterIndex;
-        if ((delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER)) != -1) {
-            // Add 1 to include the char size of the DATABASE_DELIMITER
-            return prefixedString.substring(delimiterIndex + 1);
+        synchronized (mNextPageTokensLocked) {
+            Set<Long> nextPageTokens = mNextPageTokensLocked.get(packageName);
+            if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) {
+                throw new AppSearchException(AppSearchResult.RESULT_SECURITY_ERROR,
+                        "Package \"" + packageName + "\" cannot use nextPageToken: "
+                                + nextPageToken);
+            }
         }
-        throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
-                "The prefixed value doesn't contains a valid database name.");
-    }
-
-    @NonNull
-    private static String getPrefix(@NonNull String prefixedString) throws AppSearchException {
-        int databaseDelimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
-        if (databaseDelimiterIndex == -1) {
-            throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
-                    "The databaseName prefixed value doesn't contain a valid database name.");
-        }
-
-        // Add 1 to include the char size of the DATABASE_DELIMITER
-        return prefixedString.substring(0, databaseDelimiterIndex + 1);
     }
 
     private static void addToMap(Map<String, Set<String>> map, String prefix,
@@ -1056,6 +2069,24 @@
         values.add(prefixedValue);
     }
 
+    private static void addToMap(Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix,
+            SchemaTypeConfigProto schemaTypeConfigProto) {
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
+        if (schemaTypeMap == null) {
+            schemaTypeMap = new ArrayMap<>();
+            map.put(prefix, schemaTypeMap);
+        }
+        schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
+    }
+
+    private static void removeFromMap(Map<String, Map<String, SchemaTypeConfigProto>> map,
+            String prefix, String schemaType) {
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
+        if (schemaTypeMap != null) {
+            schemaTypeMap.remove(schemaType);
+        }
+    }
+
     /**
      * Checks the given status code and throws an {@link AppSearchException} if code is an error.
      *
@@ -1085,40 +2116,86 @@
             return;
         }
 
-        throw statusProtoToAppSearchException(statusProto);
+        throw new AppSearchException(
+                ResultCodeToProtoConverter.toResultCode(statusProto.getCode()),
+                statusProto.getMessage());
     }
 
     /**
      * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources.
      *
-     * <p>This method should be only called in mutate methods and get the WRITE lock to keep thread
-     * safety.
+     * <p>This method should be only called after a mutation to local storage backend which
+     * deletes a mass of data and could release lots resources after
+     * {@link IcingSearchEngine#optimize()}.
+     *
+     * <p>This method will trigger {@link IcingSearchEngine#getOptimizeInfo()} to check
+     * resources that could be released for every {@link #CHECK_OPTIMIZE_INTERVAL} mutations.
+     *
      * <p>{@link IcingSearchEngine#optimize()} should be called only if
      * {@link GetOptimizeInfoResultProto} shows there is enough resources could be released.
-     * <p>{@link IcingSearchEngine#getOptimizeInfo()} should be called once per
-     * {@link #CHECK_OPTIMIZE_INTERVAL} of remove executions.
      *
-     * @param force whether we should directly call {@link IcingSearchEngine#getOptimizeInfo()}.
+     * @param mutationSize The number of how many mutations have been executed for current request.
+     *                     An inside counter will accumulates it. Once the counter reaches
+     *                     {@link #CHECK_OPTIMIZE_INTERVAL},
+     *                     {@link IcingSearchEngine#getOptimizeInfo()} will be triggered and the
+     *                     counter will be reset.
      */
-    @GuardedBy("mReadWriteLock")
-    private void checkForOptimizeLocked(boolean force) throws AppSearchException {
-        ++mOptimizeIntervalCountLocked;
-        if (force || mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) {
-            mOptimizeIntervalCountLocked = 0;
+    public void checkForOptimize(int mutationSize, @Nullable OptimizeStats.Builder builder)
+            throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            mOptimizeIntervalCountLocked += mutationSize;
+            if (mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) {
+                checkForOptimize(builder);
+            }
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources.
+     *
+     * <p>This method will directly trigger {@link IcingSearchEngine#getOptimizeInfo()} to check
+     * resources that could be released.
+     *
+     * <p>{@link IcingSearchEngine#optimize()} should be called only if
+     * {@link OptimizeStrategy#shouldOptimize(GetOptimizeInfoResultProto)} return true.
+     */
+    public void checkForOptimize(@Nullable OptimizeStats.Builder builder)
+            throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
             GetOptimizeInfoResultProto optimizeInfo = getOptimizeInfoResultLocked();
             checkSuccess(optimizeInfo.getStatus());
-            // Second threshold, decide when to call optimize().
-            if (optimizeInfo.getOptimizableDocs() >= OPTIMIZE_THRESHOLD_DOC_COUNT
-                    || optimizeInfo.getEstimatedOptimizableBytes()
-                    >= OPTIMIZE_THRESHOLD_BYTES) {
-                // TODO(b/155939114): call optimize in the same thread will slow down api calls
-                //  significantly. Move this call to background.
-                OptimizeResultProto optimizeResultProto = mIcingSearchEngineLocked.optimize();
-                checkSuccess(optimizeResultProto.getStatus());
+            mOptimizeIntervalCountLocked = 0;
+            if (mOptimizeStrategy.shouldOptimize(optimizeInfo)) {
+                optimize(builder);
             }
-            // TODO(b/147699081): Return OptimizeResultProto & log lost data detail once we add
-            //  a field to indicate lost_schema and lost_documents in OptimizeResultProto.
-            //  go/icing-library-apis.
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+        // TODO(b/147699081): Return OptimizeResultProto & log lost data detail once we add
+        //  a field to indicate lost_schema and lost_documents in OptimizeResultProto.
+        //  go/icing-library-apis.
+    }
+
+    /** Triggers {@link IcingSearchEngine#optimize()} directly. */
+    public void optimize(@Nullable OptimizeStats.Builder builder) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            mLogUtil.piiTrace("optimize, request");
+            OptimizeResultProto optimizeResultProto = mIcingSearchEngineLocked.optimize();
+            mLogUtil.piiTrace(
+                    "optimize, response", optimizeResultProto.getStatus(), optimizeResultProto);
+            if (builder != null) {
+                builder.setStatusCode(statusProtoToResultCode(optimizeResultProto.getStatus()));
+                AppSearchLoggerHelper.copyNativeStats(optimizeResultProto.getOptimizeStats(),
+                        builder);
+            }
+            checkSuccess(optimizeResultProto.getStatus());
+        } finally {
+            mReadWriteLock.writeLock().unlock();
         }
     }
 
@@ -1126,10 +2203,15 @@
     @NonNull
     @VisibleForTesting
     static SearchResultPage rewriteSearchResultProto(
-            @NonNull SearchResultProto searchResultProto) throws AppSearchException {
+            @NonNull SearchResultProto searchResultProto,
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+            throws AppSearchException {
         // Parallel array of package names for each document search result.
         List<String> packageNames = new ArrayList<>(searchResultProto.getResultsCount());
 
+        // Parallel array of database names for each document search result.
+        List<String> databaseNames = new ArrayList<>(searchResultProto.getResultsCount());
+
         SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder();
         for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
             SearchResultProto.ResultProto.Builder resultBuilder =
@@ -1137,54 +2219,34 @@
             DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
             String prefix = removePrefixesFromDocument(documentBuilder);
             packageNames.add(getPackageName(prefix));
+            databaseNames.add(getDatabaseName(prefix));
             resultBuilder.setDocument(documentBuilder);
             resultsBuilder.setResults(i, resultBuilder);
         }
-        return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder, packageNames);
+        return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder, packageNames,
+                databaseNames, schemaMap);
     }
 
     @GuardedBy("mReadWriteLock")
     @VisibleForTesting
     GetOptimizeInfoResultProto getOptimizeInfoResultLocked() {
-        return mIcingSearchEngineLocked.getOptimizeInfo();
-    }
-
-    @GuardedBy("mReadWriteLock")
-    @VisibleForTesting
-    VisibilityStore getVisibilityStoreLocked() {
-        return mVisibilityStoreLocked;
+        mLogUtil.piiTrace("getOptimizeInfo, request");
+        GetOptimizeInfoResultProto result = mIcingSearchEngineLocked.getOptimizeInfo();
+        mLogUtil.piiTrace("getOptimizeInfo, response", result.getStatus(), result);
+        return result;
     }
 
     /**
-     * Converts an erroneous status code to an AppSearchException. Callers should ensure that
-     * the status code is not OK or WARNING_DATA_LOSS.
+     * Converts an erroneous status code from the Icing status enums to the AppSearchResult enums.
      *
-     * @param statusProto StatusProto with error code and message to translate into
-     *                    AppSearchException.
-     * @return AppSearchException with the parallel error code.
+     * <p>Callers should ensure that the status code is not OK or WARNING_DATA_LOSS.
+     *
+     * @param statusProto StatusProto with error code to translate into an
+     *                    {@link AppSearchResult} code.
+     * @return {@link AppSearchResult} error code
      */
-    private static AppSearchException statusProtoToAppSearchException(StatusProto statusProto) {
-        switch (statusProto.getCode()) {
-            case INVALID_ARGUMENT:
-                return new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT,
-                        statusProto.getMessage());
-            case NOT_FOUND:
-                return new AppSearchException(AppSearchResult.RESULT_NOT_FOUND,
-                        statusProto.getMessage());
-            case FAILED_PRECONDITION:
-                // Fallthrough
-            case ABORTED:
-                // Fallthrough
-            case INTERNAL:
-                return new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
-                        statusProto.getMessage());
-            case OUT_OF_SPACE:
-                return new AppSearchException(AppSearchResult.RESULT_OUT_OF_SPACE,
-                        statusProto.getMessage());
-            default:
-                // Some unknown/unsupported error
-                return new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
-                        "Unknown IcingSearchEngine status code: " + statusProto.getCode());
-        }
+    private static @AppSearchResult.ResultCode int statusProtoToResultCode(
+            @NonNull StatusProto statusProto) {
+        return ResultCodeToProtoConverter.toResultCode(statusProto.getCode());
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
new file mode 100644
index 0000000..741ed1f
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
@@ -0,0 +1,77 @@
+/*
+ * 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 androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.localstorage.stats.CallStats;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.OptimizeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchStats;
+import androidx.appsearch.localstorage.stats.SetSchemaStats;
+
+/**
+ * An interface for implementing client-defined logging AppSearch operations stats.
+ *
+ * <p>Any implementation needs to provide general information on how to log all the stats types.
+ * (e.g. {@link CallStats})
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchLogger {
+    /**
+     * Logs {@link CallStats}
+     */
+    void logStats(@NonNull CallStats stats);
+
+    /**
+     * Logs {@link PutDocumentStats}
+     */
+    void logStats(@NonNull PutDocumentStats stats);
+
+    /**
+     * Logs {@link InitializeStats}
+     */
+    void logStats(@NonNull InitializeStats stats);
+
+    /**
+     * Logs {@link SearchStats}
+     */
+    void logStats(@NonNull SearchStats stats);
+
+    /**
+     * Logs {@link RemoveStats}
+     */
+    void logStats(@NonNull RemoveStats stats);
+
+    /**
+     * Logs {@link OptimizeStats}
+     */
+    void logStats(@NonNull OptimizeStats stats);
+
+    /**
+     * Logs {@link SetSchemaStats}
+     */
+    void logStats(@NonNull SetSchemaStats stats);
+
+    // TODO(b/173532925) Add remaining logStats once we add all the stats.
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
new file mode 100644
index 0000000..5f19263
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.OptimizeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchStats;
+import androidx.appsearch.localstorage.stats.SetSchemaStats;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.DeleteByQueryStatsProto;
+import com.google.android.icing.proto.DeleteStatsProto;
+import com.google.android.icing.proto.InitializeStatsProto;
+import com.google.android.icing.proto.OptimizeStatsProto;
+import com.google.android.icing.proto.PutDocumentStatsProto;
+import com.google.android.icing.proto.QueryStatsProto;
+import com.google.android.icing.proto.SetSchemaResultProto;
+
+/**
+ * Class contains helper functions for logging.
+ *
+ * <p>E.g. we need to have helper functions to copy numbers from IcingLib to stats classes.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class AppSearchLoggerHelper {
+    private AppSearchLoggerHelper() {
+    }
+
+    /**
+     * Copies native PutDocument stats to builder.
+     *
+     * @param fromNativeStats stats copied from
+     * @param toStatsBuilder  stats copied to
+     */
+    static void copyNativeStats(@NonNull PutDocumentStatsProto fromNativeStats,
+            @NonNull PutDocumentStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setNativeDocumentStoreLatencyMillis(
+                        fromNativeStats.getDocumentStoreLatencyMs())
+                .setNativeIndexLatencyMillis(fromNativeStats.getIndexLatencyMs())
+                .setNativeIndexMergeLatencyMillis(fromNativeStats.getIndexMergeLatencyMs())
+                .setNativeDocumentSizeBytes(fromNativeStats.getDocumentSize())
+                .setNativeNumTokensIndexed(
+                        fromNativeStats.getTokenizationStats().getNumTokensIndexed())
+                .setNativeExceededMaxNumTokens(
+                        fromNativeStats.getTokenizationStats().getExceededMaxTokenNum());
+    }
+
+    /**
+     * Copies native Initialize stats to builder.
+     *
+     * @param fromNativeStats stats copied from
+     * @param toStatsBuilder  stats copied to
+     */
+    static void copyNativeStats(@NonNull InitializeStatsProto fromNativeStats,
+            @NonNull InitializeStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setDocumentStoreRecoveryCause(
+                        fromNativeStats.getDocumentStoreRecoveryCause().getNumber())
+                .setIndexRestorationCause(
+                        fromNativeStats.getIndexRestorationCause().getNumber())
+                .setSchemaStoreRecoveryCause(
+                        fromNativeStats.getSchemaStoreRecoveryCause().getNumber())
+                .setDocumentStoreRecoveryLatencyMillis(
+                        fromNativeStats.getDocumentStoreRecoveryLatencyMs())
+                .setIndexRestorationLatencyMillis(
+                        fromNativeStats.getIndexRestorationLatencyMs())
+                .setSchemaStoreRecoveryLatencyMillis(
+                        fromNativeStats.getSchemaStoreRecoveryLatencyMs())
+                .setDocumentStoreDataStatus(
+                        fromNativeStats.getDocumentStoreDataStatus().getNumber())
+                .setDocumentCount(fromNativeStats.getNumDocuments())
+                .setSchemaTypeCount(fromNativeStats.getNumSchemaTypes());
+    }
+
+    /**
+     * Copies native Query stats to builder.
+     *
+     * @param fromNativeStats Stats copied from.
+     * @param toStatsBuilder Stats copied to.
+     */
+    static void copyNativeStats(@NonNull QueryStatsProto fromNativeStats,
+            @NonNull SearchStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setTermCount(fromNativeStats.getNumTerms())
+                .setQueryLength(fromNativeStats.getQueryLength())
+                .setFilteredNamespaceCount(fromNativeStats.getNumNamespacesFiltered())
+                .setFilteredSchemaTypeCount(fromNativeStats.getNumSchemaTypesFiltered())
+                .setRequestedPageSize(fromNativeStats.getRequestedPageSize())
+                .setCurrentPageReturnedResultCount(
+                        fromNativeStats.getNumResultsReturnedCurrentPage())
+                .setIsFirstPage(fromNativeStats.getIsFirstPage())
+                .setParseQueryLatencyMillis(fromNativeStats.getParseQueryLatencyMs())
+                .setRankingStrategy(fromNativeStats.getRankingStrategy().getNumber())
+                .setScoredDocumentCount(fromNativeStats.getNumDocumentsScored())
+                .setScoringLatencyMillis(fromNativeStats.getScoringLatencyMs())
+                .setRankingLatencyMillis(fromNativeStats.getRankingLatencyMs())
+                .setResultWithSnippetsCount(fromNativeStats.getNumResultsWithSnippets())
+                .setDocumentRetrievingLatencyMillis(
+                        fromNativeStats.getDocumentRetrievalLatencyMs());
+    }
+
+    /**
+     * Copies native Delete stats to builder.
+     *
+     * @param fromNativeStats Stats copied from.
+     * @param toStatsBuilder Stats copied to.
+     */
+    static void copyNativeStats(@NonNull DeleteStatsProto fromNativeStats,
+            @NonNull RemoveStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setDeleteType(fromNativeStats.getDeleteType().getNumber())
+                .setDeletedDocumentCount(fromNativeStats.getNumDocumentsDeleted());
+    }
+
+    /**
+     * Copies native DeleteByQuery stats to builder.
+     *
+     * @param fromNativeStats Stats copied from.
+     * @param toStatsBuilder Stats copied to.
+     */
+    static void copyNativeStats(@NonNull DeleteByQueryStatsProto fromNativeStats,
+            @NonNull RemoveStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+
+        @SuppressWarnings("deprecation")
+        int deleteType = DeleteStatsProto.DeleteType.Code.DEPRECATED_QUERY.getNumber();
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setDeleteType(deleteType)
+                .setDeletedDocumentCount(fromNativeStats.getNumDocumentsDeleted());
+    }
+
+    /**
+     * Copies native {@link OptimizeStatsProto} to builder.
+     *
+     * @param fromNativeStats Stats copied from.
+     * @param toStatsBuilder Stats copied to.
+     */
+    static void copyNativeStats(@NonNull OptimizeStatsProto fromNativeStats,
+            @NonNull OptimizeStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setDocumentStoreOptimizeLatencyMillis(
+                        fromNativeStats.getDocumentStoreOptimizeLatencyMs())
+                .setIndexRestorationLatencyMillis(fromNativeStats.getIndexRestorationLatencyMs())
+                .setOriginalDocumentCount(fromNativeStats.getNumOriginalDocuments())
+                .setDeletedDocumentCount(fromNativeStats.getNumDeletedDocuments())
+                .setExpiredDocumentCount(fromNativeStats.getNumExpiredDocuments())
+                .setStorageSizeBeforeBytes(fromNativeStats.getStorageSizeBefore())
+                .setStorageSizeAfterBytes(fromNativeStats.getStorageSizeAfter())
+                .setTimeSinceLastOptimizeMillis(fromNativeStats.getTimeSinceLastOptimizeMs());
+    }
+
+    /*
+     * Copy SetSchema result stats to builder.
+     *
+     * @param fromProto Stats copied from.
+     * @param toStatsBuilder Stats copied to.
+     */
+    static void copyNativeStats(@NonNull SetSchemaResultProto fromProto,
+            @NonNull SetSchemaStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromProto);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNewTypeCount(fromProto.getNewSchemaTypesCount())
+                .setDeletedTypeCount(fromProto.getDeletedSchemaTypesCount())
+                .setCompatibleTypeChangeCount(fromProto.getFullyCompatibleChangedSchemaTypesCount())
+                .setIndexIncompatibleTypeChangeCount(
+                        fromProto.getIndexIncompatibleChangedSchemaTypesCount())
+                .setBackwardsIncompatibleTypeChangeCount(
+                        fromProto.getIncompatibleSchemaTypesCount());
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
new file mode 100644
index 0000000..406e1fd
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2021 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.localstorage;
+
+import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
+import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.stats.SchemaMigrationStats;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.PersistType;
+import com.google.android.icing.protobuf.CodedInputStream;
+import com.google.android.icing.protobuf.CodedOutputStream;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The helper class for {@link AppSearchSchema} migration.
+ *
+ * <p>It will query and migrate {@link GenericDocument} in given type to a new version.
+ */
+class AppSearchMigrationHelper implements Closeable {
+    private final AppSearchImpl mAppSearchImpl;
+    private final String mPackageName;
+    private final String mDatabaseName;
+    private final File mFile;
+    private final Set<String> mDestinationTypes;
+    private boolean mAreDocumentsMigrated = false;
+
+    AppSearchMigrationHelper(@NonNull AppSearchImpl appSearchImpl,
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull Set<AppSearchSchema> newSchemas) throws IOException {
+        mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
+        mPackageName = Preconditions.checkNotNull(packageName);
+        mDatabaseName = Preconditions.checkNotNull(databaseName);
+        Preconditions.checkNotNull(newSchemas);
+        mFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null);
+        mDestinationTypes = new ArraySet<>(newSchemas.size());
+        for (AppSearchSchema newSchema : newSchemas) {
+            mDestinationTypes.add(newSchema.getSchemaType());
+        }
+    }
+
+    /**
+     * Queries all documents that need to be migrated to new version, and transform documents to
+     * new version by passing them to the provided Transformer.
+     *
+     * <p>This method will be invoked on the background worker thread.
+     *
+     * @param migrators      The map of active {@link Migrator}s that will upgrade or downgrade a
+     *                       {@link GenericDocument} to new version. The key is the schema type that
+     *                       {@link Migrator} applies to.
+     * @param currentVersion The current version of the document's schema.
+     * @param finalVersion   The final version that documents need to be migrated to.
+     *
+     * @throws IOException        on i/o problem
+     * @throws AppSearchException on AppSearch problem
+     */
+    @WorkerThread
+    public void queryAndTransform(@NonNull Map<String, Migrator> migrators, int currentVersion,
+            int finalVersion, @Nullable SchemaMigrationStats.Builder schemaMigrationStatsBuilder)
+            throws IOException, AppSearchException {
+        Preconditions.checkState(mFile.exists(), "Internal temp file does not exist.");
+        int migratedDocsCount = 0;
+        try (FileOutputStream outputStream = new FileOutputStream(mFile, /*append=*/ true)) {
+            CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(outputStream);
+            SearchResultPage searchResultPage = mAppSearchImpl.query(mPackageName, mDatabaseName,
+                    /*queryExpression=*/"",
+                    new SearchSpec.Builder()
+                            .addFilterSchemas(migrators.keySet())
+                            .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                            .build(),
+                    /*logger=*/ null);
+            while (!searchResultPage.getResults().isEmpty()) {
+                for (int i = 0; i < searchResultPage.getResults().size(); i++) {
+                    GenericDocument document =
+                            searchResultPage.getResults().get(i).getGenericDocument();
+                    Migrator migrator = migrators.get(document.getSchemaType());
+                    GenericDocument newDocument;
+                    if (currentVersion < finalVersion) {
+                        newDocument = migrator.onUpgrade(currentVersion, finalVersion, document);
+                    } else {
+                        // if current version = final version. we will return empty active
+                        // migrators at SchemaMigrationUtils.getActivityMigrators and won't reach
+                        // here.
+                        newDocument = migrator.onDowngrade(currentVersion, finalVersion, document);
+                    }
+                    if (!mDestinationTypes.contains(newDocument.getSchemaType())) {
+                        // we exit before the new schema has been set to AppSearch. So no
+                        // observable changes will be applied to stored schemas and documents.
+                        // And the temp file will be deleted at close(), which will be triggered at
+                        // the end of try-with-resources when using AppSearchMigrationHelper.
+                        throw new AppSearchException(RESULT_INVALID_SCHEMA,
+                                "Receive a migrated document with schema type: "
+                                        + newDocument.getSchemaType()
+                                        + ". But the schema types doesn't exist in the request");
+                    }
+                    Bundle bundle = newDocument.getBundle();
+                    byte[] serializedMessage;
+                    Parcel parcel = Parcel.obtain();
+                    try {
+                        parcel.writeBundle(bundle);
+                        serializedMessage = parcel.marshall();
+                    } finally {
+                        parcel.recycle();
+                    }
+                    codedOutputStream.writeByteArrayNoTag(serializedMessage);
+                }
+                codedOutputStream.flush();
+                migratedDocsCount += searchResultPage.getResults().size();
+                searchResultPage = mAppSearchImpl.getNextPage(mPackageName,
+                        searchResultPage.getNextPageToken());
+                outputStream.flush();
+            }
+        }
+        mAreDocumentsMigrated = true;
+        if (schemaMigrationStatsBuilder != null) {
+            schemaMigrationStatsBuilder.setMigratedDocumentCount(migratedDocsCount);
+        }
+    }
+
+    /**
+     * Reads {@link GenericDocument} from the temperate file and saves them to AppSearch.
+     *
+     * <p> This method should be only called once.
+     *
+     * @param responseBuilder a SetSchemaResponse builder whose result will be returned by this
+     *                        function with any
+     *                        {@link androidx.appsearch.app.SetSchemaResponse.MigrationFailure}
+     *                        added in.
+     * @return  the {@link SetSchemaResponse} for this
+     *          {@link androidx.appsearch.app.AppSearchSession#setSchema} call.
+     *
+     * @throws IOException        on i/o problem
+     * @throws AppSearchException on AppSearch problem
+     */
+    @NonNull
+    @WorkerThread
+    public SetSchemaResponse readAndPutDocuments(@NonNull SetSchemaResponse.Builder responseBuilder,
+            SchemaMigrationStats.Builder schemaMigrationStatsBuilder)
+            throws IOException, AppSearchException {
+        Preconditions.checkState(mFile.exists(), "Internal temp file does not exist.");
+        if (!mAreDocumentsMigrated) {
+            return responseBuilder.build();
+        }
+        try (InputStream inputStream = new FileInputStream(mFile)) {
+            CodedInputStream codedInputStream = CodedInputStream.newInstance(inputStream);
+            int savedDocsCount = 0;
+            while (!codedInputStream.isAtEnd()) {
+                GenericDocument document = readDocumentFromInputStream(codedInputStream);
+                try {
+                    mAppSearchImpl.putDocument(mPackageName, mDatabaseName, document,
+                            /*logger=*/ null);
+                    savedDocsCount++;
+                } catch (Throwable t) {
+                    responseBuilder.addMigrationFailure(
+                            new SetSchemaResponse.MigrationFailure(
+                                    document.getNamespace(),
+                                    document.getId(),
+                                    document.getSchemaType(),
+                                    throwableToFailedResult(t)));
+                }
+            }
+            mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
+            if (schemaMigrationStatsBuilder != null) {
+                schemaMigrationStatsBuilder.setSavedDocumentCount(savedDocsCount);
+            }
+        }
+        return responseBuilder.build();
+    }
+
+    /**
+     * Reads {@link GenericDocument} from given {@link CodedInputStream}.
+     *
+     * @param codedInputStream The codedInputStream to read from
+     *
+     * @throws IOException        on File operation error.
+     */
+    @NonNull
+    private static GenericDocument readDocumentFromInputStream(
+            @NonNull CodedInputStream codedInputStream) throws IOException {
+        byte[] serializedMessage = codedInputStream.readByteArray();
+
+        Bundle bundle;
+        Parcel parcel = Parcel.obtain();
+        try {
+            parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
+            parcel.setDataPosition(0);
+            bundle = parcel.readBundle();
+        } finally {
+            parcel.recycle();
+        }
+
+        return new GenericDocument(bundle);
+    }
+
+    @Override
+    public void close() {
+        mFile.delete();
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
index ac8fec2..fbb7995 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
@@ -16,17 +16,24 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.localstorage;
 
+import android.content.Context;
+
 import androidx.annotation.NonNull;
-import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.util.FutureUtil;
 import androidx.core.util.Preconditions;
 
-import java.util.concurrent.ExecutorService;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
 
 /**
- * An implementation of {@link AppSearchSession} which stores data locally
+ * An implementation of {@link GlobalSearchSession} which stores data locally
  * in the app's storage space using a bundled version of the search native library.
  *
  * <p>Queries are executed multi-threaded, but a single thread is used for mutate requests (put,
@@ -34,27 +41,56 @@
  */
 class GlobalSearchSessionImpl implements GlobalSearchSession {
     private final AppSearchImpl mAppSearchImpl;
-    private final ExecutorService mExecutorService;
+    private final Executor mExecutor;
+    private final Context mContext;
+
+    private boolean mIsClosed = false;
 
     GlobalSearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
-            @NonNull ExecutorService executorService) {
+            @NonNull Executor executor,
+            @NonNull Context context) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
-        mExecutorService = Preconditions.checkNotNull(executorService);
+        mExecutor = Preconditions.checkNotNull(executor);
+        mContext = Preconditions.checkNotNull(context);
     }
 
     @NonNull
     @Override
-    public SearchResults query(
+    public SearchResults search(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
+        Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
         return new SearchResultsImpl(
                 mAppSearchImpl,
-                mExecutorService,
-                /*packageName=*/ null,
+                mExecutor,
+                mContext.getPackageName(),
                 /*databaseName=*/ null,
                 queryExpression,
                 searchSpec);
     }
+
+    /**
+     * Reporting system usage is not supported in the local backend, so this method does nothing
+     * and always completes the return value with an
+     * {@link androidx.appsearch.exceptions.AppSearchException} having a result code of
+     * {@link AppSearchResult#RESULT_SECURITY_ERROR}.
+     */
+    @NonNull
+    @Override
+    public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+        return FutureUtil.execute(mExecutor, () -> {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_SECURITY_ERROR,
+                    mContext.getPackageName() + " does not have access to report system usage");
+        });
+    }
+
+    @Override
+    public void close() {
+        mIsClosed = true;
+    }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/JetpackOptimizeStrategy.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/JetpackOptimizeStrategy.java
new file mode 100644
index 0000000..6c0ee68
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/JetpackOptimizeStrategy.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.android.icing.proto.GetOptimizeInfoResultProto;
+
+/**
+ * An implementation of {@link androidx.appsearch.localstorage.OptimizeStrategy} will
+ * determine when to trigger {@link androidx.appsearch.localstorage.AppSearchImpl#optimize()} in
+ * Jetpack environment.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JetpackOptimizeStrategy implements OptimizeStrategy{
+
+    @VisibleForTesting
+    static final int DOC_COUNT_OPTIMIZE_THRESHOLD = 1000;
+    @VisibleForTesting
+    static final int BYTES_OPTIMIZE_THRESHOLD = 1 * 1024 * 1024; // 1MB
+
+    @Override
+    public boolean shouldOptimize(@NonNull GetOptimizeInfoResultProto optimizeInfo) {
+        return optimizeInfo.getOptimizableDocs() >= DOC_COUNT_OPTIMIZE_THRESHOLD
+                || optimizeInfo.getEstimatedOptimizableBytes() >= BYTES_OPTIMIZE_THRESHOLD;
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LimitConfig.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LimitConfig.java
new file mode 100644
index 0000000..dded201
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LimitConfig.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Defines limits placed on users of AppSearch and enforced by {@link AppSearchImpl}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface LimitConfig {
+    /**
+     * The maximum number of bytes a single document is allowed to be.
+     *
+     * <p>Enforced at the time of serializing the document into a proto.
+     *
+     * <p>This limit has two purposes:
+     * <ol>
+     *     <li>Prevent the system service from using too much memory during indexing or querying
+     *     by capping the size of the data structures it needs to buffer
+     *     <li>Prevent apps from using a very large amount of data by storing exceptionally large
+     *     documents.
+     * </ol>
+     */
+    int getMaxDocumentSizeBytes();
+
+    /**
+     * The maximum number of documents a single app is allowed to index.
+     *
+     * <p>Enforced at indexing time.
+     *
+     * <p>This limit has two purposes:
+     * <ol>
+     *     <li>Protect icing lib's docid space from being overwhelmed by a single app. The
+     *     overall docid limit is currently 2^20 (~1 million)
+     *     <li>Prevent apps from using a very large amount of data on the system by storing too many
+     *     documents.
+     * </ol>
+     */
+    int getMaxDocumentCount();
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 8f6cfde..85e7521 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -17,20 +17,27 @@
 package androidx.appsearch.localstorage;
 
 import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
+import androidx.appsearch.annotation.Document;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.OptimizeStats;
 import androidx.appsearch.localstorage.util.FutureUtil;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.io.File;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -40,20 +47,14 @@
  *
  * <p>The search native library is an on-device searching library that allows apps to define
  * {@link androidx.appsearch.app.AppSearchSchema}s, save and query a variety of
- * {@link androidx.appsearch.annotation.AppSearchDocument}s. The library needs to be initialized
+ * {@link Document}s. The library needs to be initialized
  * before using, which will create a folder to save data in the app's storage space.
  *
  * <p>Queries are executed multi-threaded, but a single thread is used for mutate requests (put,
  * delete, etc..).
  */
 public class LocalStorage {
-    /**
-     * The default empty database name.
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @VisibleForTesting
-    public static final String DEFAULT_DATABASE_NAME = "";
+    private static final String TAG = "AppSearchLocalStorage";
 
     private static final String ICING_LIB_ROOT_DIR = "appsearch";
 
@@ -61,109 +62,183 @@
     public static final class SearchContext {
         final Context mContext;
         final String mDatabaseName;
+        final Executor mExecutor;
+        final AppSearchLogger mLogger;
 
-        SearchContext(@NonNull Context context, @NonNull String databaseName) {
+        SearchContext(@NonNull Context context, @NonNull String databaseName,
+                @NonNull Executor executor, @Nullable AppSearchLogger logger) {
             mContext = Preconditions.checkNotNull(context);
             mDatabaseName = Preconditions.checkNotNull(databaseName);
+            mExecutor = Preconditions.checkNotNull(executor);
+            mLogger = logger;
         }
 
         /**
          * Returns the name of the database to create or open.
-         *
-         * <p>Databases with different names are fully separate with distinct types, namespaces,
-         * and data.
          */
         @NonNull
         public String getDatabaseName() {
             return mDatabaseName;
         }
 
+        /**
+         * Returns the worker executor associated with {@link AppSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
+        }
+
         /** Builder for {@link SearchContext} objects. */
         public static final class Builder {
             private final Context mContext;
-            private String mDatabaseName = DEFAULT_DATABASE_NAME;
-            private boolean mBuilt = false;
-
-            public Builder(@NonNull Context context) {
-                mContext = Preconditions.checkNotNull(context);
-            }
+            private final String mDatabaseName;
+            private Executor mExecutor;
+            private AppSearchLogger mLogger;
 
             /**
-             * Sets the name of the database associated with {@link AppSearchSession}.
+             * Creates a {@link SearchContext.Builder} instance.
              *
              * <p>{@link AppSearchSession} will create or open a database under the given name.
              *
-             * <p>Databases with different names are fully separate with distinct types, namespaces,
-             * and data.
+             * <p>Databases with different names are fully separate with distinct schema types,
+             * namespaces, and documents.
              *
-             * <p>Database name cannot contain {@code '/'}.
-             *
-             * <p>If not specified, defaults to the empty string.
+             * <p>The database name cannot contain {@code '/'}.
              *
              * @param databaseName The name of the database.
              * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
              */
-            @NonNull
-            public Builder setDatabaseName(@NonNull String databaseName) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
+            public Builder(@NonNull Context context, @NonNull String databaseName) {
+                mContext = Preconditions.checkNotNull(context);
                 Preconditions.checkNotNull(databaseName);
                 if (databaseName.contains("/")) {
                     throw new IllegalArgumentException("Database name cannot contain '/'");
                 }
                 mDatabaseName = databaseName;
+            }
+
+            /**
+             * Sets the worker executor associated with {@link AppSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                mExecutor = Preconditions.checkNotNull(executor);
+                return this;
+            }
+
+
+            /**
+             * Sets the custom logger used to get the details stats from AppSearch.
+             *
+             * <p>If no logger is provided, nothing would be returned/logged. There is no default
+             * logger implementation in AppSearch.
+             *
+             * @hide
+             */
+            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+            @NonNull
+            public Builder setLogger(@NonNull AppSearchLogger logger) {
+                mLogger = Preconditions.checkNotNull(logger);
                 return this;
             }
 
             /** Builds a {@link SearchContext} instance. */
             @NonNull
             public SearchContext build() {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                mBuilt = true;
-                return new SearchContext(mContext, mDatabaseName);
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
+                return new SearchContext(mContext, mDatabaseName, mExecutor, mLogger);
             }
         }
     }
 
     /**
      * Contains information relevant to creating a global search session.
+     *
+     * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static final class GlobalSearchContext {
         final Context mContext;
+        final Executor mExecutor;
 
-        GlobalSearchContext(@NonNull Context context) {
+        GlobalSearchContext(@NonNull Context context, @NonNull Executor executor) {
             mContext = Preconditions.checkNotNull(context);
+            mExecutor = Preconditions.checkNotNull(executor);
+        }
+
+        /**
+         * Returns the worker executor associated with {@link GlobalSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
         }
 
         /** Builder for {@link GlobalSearchContext} objects. */
         public static final class Builder {
             private final Context mContext;
-            private boolean mBuilt = false;
+            private Executor mExecutor;
 
             public Builder(@NonNull Context context) {
                 mContext = Preconditions.checkNotNull(context);
             }
 
+            /**
+             * Sets the worker executor associated with {@link GlobalSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                Preconditions.checkNotNull(executor);
+                mExecutor = executor;
+                return this;
+            }
+
             /** Builds a {@link GlobalSearchContext} instance. */
             @NonNull
             public GlobalSearchContext build() {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                mBuilt = true;
-                return new GlobalSearchContext(mContext);
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
+                return new GlobalSearchContext(mContext, mExecutor);
             }
         }
     }
 
-    // Never call Executor.shutdownNow(), it will cancel the futures it's returned. And since
-    // execute() won't return anything, we will hang forever waiting for the execution.
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+
     private static volatile LocalStorage sInstance;
 
     private final AppSearchImpl mAppSearchImpl;
 
     /**
-     * Opens a new {@link AppSearchSession} on this storage.
+     * Opens a new {@link AppSearchSession} on this storage with executor.
      *
      * <p>This process requires a native search library. If it's not created, the initialization
      * process will create one.
@@ -175,30 +250,10 @@
     public static ListenableFuture<AppSearchSession> createSearchSession(
             @NonNull SearchContext context) {
         Preconditions.checkNotNull(context);
-        return createSearchSession(context, EXECUTOR_SERVICE);
-    }
-
-    /**
-     * Opens a new {@link AppSearchSession} on this storage with executor.
-     *
-     * <p>This process requires a native search library. If it's not created, the initialization
-     * process will create one.
-     *
-     * @param context  The {@link SearchContext} contains all information to create a new
-     *                 {@link AppSearchSession}
-     * @param executor The executor of where tasks will execute.
-     * @hide
-     */
-    @NonNull
-    @VisibleForTesting
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static ListenableFuture<AppSearchSession> createSearchSession(
-            @NonNull SearchContext context, @NonNull ExecutorService executor) {
-        Preconditions.checkNotNull(context);
-        Preconditions.checkNotNull(executor);
-        return FutureUtil.execute(executor, () -> {
-            LocalStorage instance = getOrCreateInstance(context.mContext);
-            return instance.doCreateSearchSession(context, executor);
+        return FutureUtil.execute(context.mExecutor, () -> {
+            LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor,
+                    context.mLogger);
+            return instance.doCreateSearchSession(context);
         });
     }
 
@@ -208,17 +263,17 @@
      * <p>This process requires a native search library. If it's not created, the initialization
      * process will create one.
      *
-     * @param context The {@link GlobalSearchContext} contains all information to create a new
-     *                {@link GlobalSearchSession}
      * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
     public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
             @NonNull GlobalSearchContext context) {
         Preconditions.checkNotNull(context);
-        return FutureUtil.execute(EXECUTOR_SERVICE, () -> {
-            LocalStorage instance = getOrCreateInstance(context.mContext);
-            return instance.doCreateGlobalSearchSession(EXECUTOR_SERVICE);
+        return FutureUtil.execute(context.mExecutor, () -> {
+            LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor,
+                    /*logger=*/ null);
+            return instance.doCreateGlobalSearchSession(context);
         });
     }
 
@@ -231,12 +286,14 @@
     @NonNull
     @WorkerThread
     @VisibleForTesting
-    static LocalStorage getOrCreateInstance(@NonNull Context context) throws AppSearchException {
+    static LocalStorage getOrCreateInstance(@NonNull Context context, @NonNull Executor executor,
+            @Nullable AppSearchLogger logger)
+            throws AppSearchException {
         Preconditions.checkNotNull(context);
         if (sInstance == null) {
             synchronized (LocalStorage.class) {
                 if (sInstance == null) {
-                    sInstance = new LocalStorage(context);
+                    sInstance = new LocalStorage(context, executor, logger);
                 }
             }
         }
@@ -244,21 +301,71 @@
     }
 
     @WorkerThread
-    private LocalStorage(@NonNull Context context) throws AppSearchException {
+    private LocalStorage(
+            @NonNull Context context,
+            @NonNull Executor executor,
+            @Nullable AppSearchLogger logger)
+            throws AppSearchException {
         Preconditions.checkNotNull(context);
         File icingDir = new File(context.getFilesDir(), ICING_LIB_ROOT_DIR);
-        mAppSearchImpl = AppSearchImpl.create(icingDir);
+
+        long totalLatencyStartMillis = SystemClock.elapsedRealtime();
+        InitializeStats.Builder initStatsBuilder = null;
+        if (logger != null) {
+            initStatsBuilder = new InitializeStats.Builder();
+        }
+
+        mAppSearchImpl = AppSearchImpl.create(
+                icingDir,
+                new UnlimitedLimitConfig(),
+                initStatsBuilder,
+                new JetpackOptimizeStrategy());
+
+        if (logger != null) {
+            initStatsBuilder.setTotalLatencyMillis(
+                    (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
+            logger.logStats(initStatsBuilder.build());
+        }
+
+        executor.execute(() -> {
+            long totalOptimizeLatencyStartMillis = SystemClock.elapsedRealtime();
+            OptimizeStats.Builder builder = null;
+            try {
+                if (logger != null) {
+                    builder = new OptimizeStats.Builder();
+                }
+                mAppSearchImpl.checkForOptimize(builder);
+            } catch (AppSearchException e) {
+                Log.w(TAG, "Error occurred when check for optimize", e);
+            } finally {
+                if (builder != null) {
+                    OptimizeStats oStats = builder
+                            .setTotalLatencyMillis(
+                                    (int) (SystemClock.elapsedRealtime()
+                                            - totalOptimizeLatencyStartMillis))
+                            .build();
+                    if (logger != null && oStats.getOriginalDocumentCount() > 0) {
+                        // see if optimize has been run by checking originalDocumentCount
+                        logger.logStats(builder.build());
+                    }
+                }
+            }
+        });
     }
 
     @NonNull
-    private AppSearchSession doCreateSearchSession(@NonNull SearchContext context,
-            @NonNull ExecutorService executor) {
-        return new SearchSessionImpl(mAppSearchImpl, executor,
-                context.mContext.getPackageName(), context.mDatabaseName);
+    private AppSearchSession doCreateSearchSession(@NonNull SearchContext context) {
+        return new SearchSessionImpl(
+                mAppSearchImpl,
+                context.mExecutor,
+                context.mContext.getPackageName(),
+                context.mDatabaseName,
+                context.mLogger);
     }
 
     @NonNull
-    private GlobalSearchSession doCreateGlobalSearchSession(@NonNull ExecutorService executor) {
-        return new GlobalSearchSessionImpl(mAppSearchImpl, executor);
+    private GlobalSearchSession doCreateGlobalSearchSession(
+            @NonNull GlobalSearchContext context) {
+        return new GlobalSearchSessionImpl(mAppSearchImpl, context.mExecutor, context.mContext);
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/OptimizeStrategy.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/OptimizeStrategy.java
new file mode 100644
index 0000000..962dc6b
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/OptimizeStrategy.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import com.google.android.icing.proto.GetOptimizeInfoResultProto;
+
+/**
+ * An interface class for implementing a strategy to determine when to trigger
+ * {@link AppSearchImpl#optimize()}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface OptimizeStrategy {
+
+    /**
+     * Determines whether {@link AppSearchImpl#optimize()} need to be triggered to release garbage
+     * resources in AppSearch base on the given information.
+     *
+     * @param optimizeInfo The proto object indicates the number of garbage resources in AppSearch.
+     * @return {@code true} if {@link AppSearchImpl#optimize()} need to be triggered.
+     */
+    boolean shouldOptimize(@NonNull GetOptimizeInfoResultProto optimizeInfo);
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
index e11f1f0..f38d08b 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
@@ -16,6 +16,8 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.localstorage;
 
+import android.os.Process;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appsearch.app.AppSearchResult;
@@ -30,12 +32,12 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.List;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
 
 class SearchResultsImpl implements SearchResults {
     private final AppSearchImpl mAppSearchImpl;
 
-    private final ExecutorService mExecutorService;
+    private final Executor mExecutor;
 
     // The package name to search over. If null, this will search over all package names.
     @Nullable
@@ -57,13 +59,13 @@
 
     SearchResultsImpl(
             @NonNull AppSearchImpl appSearchImpl,
-            @NonNull ExecutorService executorService,
+            @NonNull Executor executor,
             @Nullable String packageName,
             @Nullable String databaseName,
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
-        mExecutorService = Preconditions.checkNotNull(executorService);
+        mExecutor = Preconditions.checkNotNull(executor);
         mPackageName = packageName;
         mDatabaseName = databaseName;
         mQueryExpression = Preconditions.checkNotNull(queryExpression);
@@ -74,28 +76,32 @@
     @NonNull
     public ListenableFuture<List<SearchResult>> getNextPage() {
         Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
-        return FutureUtil.execute(mExecutorService, () -> {
+        return FutureUtil.execute(mExecutor, () -> {
             SearchResultPage searchResultPage;
             if (mIsFirstLoad) {
                 mIsFirstLoad = false;
-                if (mDatabaseName == null && mPackageName == null) {
-                    // Global query, there's no one package-database combination to check.
-                    searchResultPage = mAppSearchImpl.globalQuery(mQueryExpression, mSearchSpec);
-                } else if (mPackageName == null) {
+                if (mPackageName == null) {
                     throw new AppSearchException(
                             AppSearchResult.RESULT_INVALID_ARGUMENT,
                             "Invalid null package name for query");
                 } else if (mDatabaseName == null) {
-                    throw new AppSearchException(
-                            AppSearchResult.RESULT_INVALID_ARGUMENT,
-                            "Invalid null database name for query");
+                    // Global queries aren't restricted to a single database
+                    searchResultPage = mAppSearchImpl.globalQuery(
+                            mQueryExpression,
+                            mSearchSpec,
+                            mPackageName,
+                            /*visibilityStore=*/ null,
+                            Process.myUid(),
+                            /*callerHasSystemAccess=*/ false,
+                            /*logger=*/ null);
                 } else {
                     // Normal local query, pass in specified database.
                     searchResultPage = mAppSearchImpl.query(
-                            mPackageName, mDatabaseName, mQueryExpression, mSearchSpec);
+                            mPackageName, mDatabaseName, mQueryExpression, mSearchSpec, /*logger
+                            =*/ null);
                 }
             } else {
-                searchResultPage = mAppSearchImpl.getNextPage(mNextPageToken);
+                searchResultPage = mAppSearchImpl.getNextPage(mPackageName, mNextPageToken);
             }
             mNextPageToken = searchResultPage.getNextPageToken();
             return searchResultPage.getResults();
@@ -108,8 +114,8 @@
         // Checking the future result is not needed here since this is a cleanup step which is not
         // critical to the correct functioning of the system; also, the return value is void.
         if (!mIsClosed) {
-            FutureUtil.execute(mExecutorService, () -> {
-                mAppSearchImpl.invalidateNextPageToken(mNextPageToken);
+            FutureUtil.execute(mExecutor, () -> {
+                mAppSearchImpl.invalidateNextPageToken(mPackageName, mNextPageToken);
                 mIsClosed = true;
                 return null;
             });
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 973b724..4743841 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -18,123 +18,298 @@
 
 import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
 
+import android.os.SystemClock;
+import android.util.Log;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appsearch.app.AppSearchBatchResult;
-import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByUriRequest;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.PutDocumentsRequest;
-import androidx.appsearch.app.RemoveByUriRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.stats.OptimizeStats;
+import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SchemaMigrationStats;
+import androidx.appsearch.localstorage.stats.SetSchemaStats;
 import androidx.appsearch.localstorage.util.FutureUtil;
+import androidx.appsearch.util.SchemaMigrationUtil;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.PersistType;
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
 
 /**
- * An implementation of {@link AppSearchSession} which stores data locally
- * in the app's storage space using a bundled version of the search native library.
+ * An implementation of {@link AppSearchSession} which stores data locally in the app's storage
+ * space using a bundled version of the search native library.
  *
  * <p>Queries are executed multi-threaded, but a single thread is used for mutate requests (put,
  * delete, etc..).
  */
 class SearchSessionImpl implements AppSearchSession {
+    private static final String TAG = "AppSearchSessionImpl";
     private final AppSearchImpl mAppSearchImpl;
-    private final ExecutorService mExecutorService;
+    private final Executor mExecutor;
     private final String mPackageName;
     private final String mDatabaseName;
-    private boolean mIsMutated = false;
-    private boolean mIsClosed = false;
+    private volatile boolean mIsMutated = false;
+    private volatile boolean mIsClosed = false;
+    @Nullable private final AppSearchLogger mLogger;
 
     SearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
-            @NonNull ExecutorService executorService,
+            @NonNull Executor executor,
             @NonNull String packageName,
-            @NonNull String databaseName) {
+            @NonNull String databaseName,
+            @Nullable AppSearchLogger logger) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
-        mExecutorService = Preconditions.checkNotNull(executorService);
+        mExecutor = Preconditions.checkNotNull(executor);
         mPackageName = packageName;
         mDatabaseName = Preconditions.checkNotNull(databaseName);
+        mLogger = logger;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<Void> setSchema(@NonNull SetSchemaRequest request) {
+    public ListenableFuture<SetSchemaResponse> setSchema(
+            @NonNull SetSchemaRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return execute(() -> {
-            mAppSearchImpl.setSchema(
+
+        ListenableFuture<SetSchemaResponse> future = execute(() -> {
+            long startMillis = SystemClock.elapsedRealtime();
+
+            // Convert the inner set into a List since Binder can't handle Set.
+            Map<String, Set<PackageIdentifier>> schemasVisibleToPackages =
+                    request.getSchemasVisibleToPackagesInternal();
+            Map<String, List<PackageIdentifier>> copySchemasVisibleToPackages = new ArrayMap<>();
+            for (Map.Entry<String, Set<PackageIdentifier>> entry :
+                    schemasVisibleToPackages.entrySet()) {
+                copySchemasVisibleToPackages.put(entry.getKey(),
+                        new ArrayList<>(entry.getValue()));
+            }
+
+            SetSchemaStats.Builder setSchemaStatsBuilder = null;
+            if (mLogger != null) {
+                setSchemaStatsBuilder = new SetSchemaStats.Builder(mPackageName, mDatabaseName);
+            }
+
+            Map<String, Migrator> migrators = request.getMigrators();
+            // No need to trigger migration if user never set migrator.
+            if (migrators.size() == 0) {
+                SetSchemaResponse setSchemaResponse =
+                        setSchemaNoMigrations(request, copySchemasVisibleToPackages,
+                                setSchemaStatsBuilder);
+                if (setSchemaStatsBuilder != null) {
+                    setSchemaStatsBuilder.setTotalLatencyMillis(
+                            (int) (SystemClock.elapsedRealtime() - startMillis));
+                    mLogger.logStats(setSchemaStatsBuilder.build());
+                }
+                return setSchemaResponse;
+            }
+
+            // Migration process
+            // 1. Validate and retrieve all active migrators.
+            GetSchemaResponse getSchemaResponse =
+                    mAppSearchImpl.getSchema(mPackageName, mDatabaseName);
+            int currentVersion = getSchemaResponse.getVersion();
+            int finalVersion = request.getVersion();
+            Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
+                    getSchemaResponse.getSchemas(), migrators, currentVersion, finalVersion);
+            // No need to trigger migration if no migrator is active.
+            if (activeMigrators.size() == 0) {
+                SetSchemaResponse setSchemaResponse =
+                        setSchemaNoMigrations(request, copySchemasVisibleToPackages,
+                                setSchemaStatsBuilder);
+                if (setSchemaStatsBuilder != null) {
+                    setSchemaStatsBuilder.setTotalLatencyMillis(
+                            (int) (SystemClock.elapsedRealtime() - startMillis));
+                    mLogger.logStats(setSchemaStatsBuilder.build());
+                }
+                return setSchemaResponse;
+            }
+
+            // 2. SetSchema with forceOverride=false, to retrieve the list of incompatible/deleted
+            // types.
+            long firstSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
+            SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema(
                     mPackageName,
                     mDatabaseName,
                     new ArrayList<>(request.getSchemas()),
-                    new ArrayList<>(request.getSchemasNotVisibleToSystemUi()),
-                    request.isForceOverride());
-            mIsMutated = true;
-            return null;
+                    /*visibilityStore=*/ null,
+                    new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
+                    copySchemasVisibleToPackages,
+                    /*forceOverride=*/false,
+                    request.getVersion(),
+                    setSchemaStatsBuilder);
+
+            // 3. If forceOverride is false, check that all incompatible types will be migrated.
+            // If some aren't we must throw an error, rather than proceeding and deleting those
+            // types.
+            long queryAndTransformLatencyStartMillis = SystemClock.elapsedRealtime();
+            if (!request.isForceOverride()) {
+                SchemaMigrationUtil.checkDeletedAndIncompatibleAfterMigration(setSchemaResponse,
+                        activeMigrators.keySet());
+            }
+
+            SchemaMigrationStats.Builder schemaMigrationStatsBuilder = null;
+            if (setSchemaStatsBuilder != null) {
+                schemaMigrationStatsBuilder = new SchemaMigrationStats.Builder();
+            }
+
+            try (AppSearchMigrationHelper migrationHelper = new AppSearchMigrationHelper(
+                    mAppSearchImpl, mPackageName, mDatabaseName, request.getSchemas())) {
+                // 4. Trigger migration for all activity migrators.
+                migrationHelper.queryAndTransform(activeMigrators, currentVersion, finalVersion,
+                        schemaMigrationStatsBuilder);
+
+                // 5. SetSchema a second time with forceOverride=true if the first attempted failed
+                // due to backward incompatible changes.
+                long secondSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
+                if (!setSchemaResponse.getIncompatibleTypes().isEmpty()
+                        || !setSchemaResponse.getDeletedTypes().isEmpty()) {
+                    setSchemaResponse = mAppSearchImpl.setSchema(
+                            mPackageName,
+                            mDatabaseName,
+                            new ArrayList<>(request.getSchemas()),
+                            /*visibilityStore=*/ null,
+                            new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
+                            copySchemasVisibleToPackages,
+                            /*forceOverride=*/ true,
+                            request.getVersion(),
+                            setSchemaStatsBuilder);
+                }
+                SetSchemaResponse.Builder responseBuilder = setSchemaResponse.toBuilder()
+                        .addMigratedTypes(activeMigrators.keySet());
+                mIsMutated = true;
+
+                // 6. Put all the migrated documents into the index, now that the new schema is set.
+                long saveDocumentLatencyStartMillis = SystemClock.elapsedRealtime();
+                SetSchemaResponse finalSetSchemaResponse =
+                        migrationHelper.readAndPutDocuments(responseBuilder,
+                                schemaMigrationStatsBuilder);
+
+                if (schemaMigrationStatsBuilder != null) {
+                    long endMillis = SystemClock.elapsedRealtime();
+                    schemaMigrationStatsBuilder
+                            .setSaveDocumentLatencyMillis(
+                                    (int) (endMillis - saveDocumentLatencyStartMillis))
+                            .setGetSchemaLatencyMillis(
+                                    (int) (firstSetSchemaLatencyStartMillis - startMillis))
+                            .setFirstSetSchemaLatencyMillis(
+                                    (int) (queryAndTransformLatencyStartMillis
+                                            - firstSetSchemaLatencyStartMillis))
+                            .setQueryAndTransformLatencyMillis(
+                                    (int) (secondSetSchemaLatencyStartMillis
+                                            - queryAndTransformLatencyStartMillis))
+                            .setSecondSetSchemaLatencyMillis(
+                                    (int) (saveDocumentLatencyStartMillis
+                                            - secondSetSchemaLatencyStartMillis));
+                    setSchemaStatsBuilder
+                            .setSchemaMigrationStats(
+                                    schemaMigrationStatsBuilder.build())
+                            .setTotalLatencyMillis((int) (endMillis - startMillis));
+                    mLogger.logStats(setSchemaStatsBuilder.build());
+                }
+
+                return finalSetSchemaResponse;
+            }
         });
+
+        // setSchema will sync the schemas in the request to AppSearch, any existing schemas which
+        // is not included in the request will be delete if we force override incompatible schemas.
+        // And all documents of these types will be deleted as well. We should checkForOptimize for
+        // these deletion.
+        checkForOptimize();
+        return future;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<Set<AppSearchSchema>> getSchema() {
+    public ListenableFuture<GetSchemaResponse> getSchema() {
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+        return execute(() -> mAppSearchImpl.getSchema(mPackageName, mDatabaseName));
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Set<String>> getNamespaces() {
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
-            List<AppSearchSchema> schemas = mAppSearchImpl.getSchema(mPackageName, mDatabaseName);
-            return new ArraySet<>(schemas);
+            List<String> namespaces = mAppSearchImpl.getNamespaces(mPackageName, mDatabaseName);
+            return new ArraySet<>(namespaces);
         });
     }
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> putDocuments(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> put(
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return execute(() -> {
+        ListenableFuture<AppSearchBatchResult<String, Void>> future = execute(() -> {
             AppSearchBatchResult.Builder<String, Void> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
-            for (int i = 0; i < request.getDocuments().size(); i++) {
-                GenericDocument document = request.getDocuments().get(i);
+            for (int i = 0; i < request.getGenericDocuments().size(); i++) {
+                GenericDocument document = request.getGenericDocuments().get(i);
                 try {
-                    mAppSearchImpl.putDocument(mPackageName, mDatabaseName, document);
-                    resultBuilder.setSuccess(document.getUri(), /*result=*/ null);
+                    mAppSearchImpl.putDocument(mPackageName, mDatabaseName, document, mLogger);
+                    resultBuilder.setSuccess(document.getId(), /*value=*/ null);
                 } catch (Throwable t) {
-                    resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
+                    resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
                 }
             }
+            // Now that the batch has been written. Persist the newly written data.
+            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
             mIsMutated = true;
             return resultBuilder.build();
         });
+
+        // The existing documents with same ID will be deleted, so there may be some resources that
+        // could be released after optimize().
+        checkForOptimize(/*mutateBatchSize=*/ request.getGenericDocuments().size());
+        return future;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByUri(
-            @NonNull GetByUriRequest request) {
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
 
-            for (String uri : request.getUris()) {
+            Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+            for (String id : request.getIds()) {
                 try {
                     GenericDocument document =
                             mAppSearchImpl.getDocument(mPackageName, mDatabaseName,
-                                    request.getNamespace(), uri);
-                    resultBuilder.setSuccess(uri, document);
+                                    request.getNamespace(), id, typePropertyPaths);
+                    resultBuilder.setSuccess(id, document);
                 } catch (Throwable t) {
-                    resultBuilder.setResult(uri, throwableToFailedResult(t));
+                    resultBuilder.setResult(id, throwableToFailedResult(t));
                 }
             }
             return resultBuilder.build();
@@ -143,7 +318,7 @@
 
     @Override
     @NonNull
-    public SearchResults query(
+    public SearchResults search(
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
@@ -151,7 +326,7 @@
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return new SearchResultsImpl(
                 mAppSearchImpl,
-                mExecutorService,
+                mExecutor,
                 mPackageName,
                 mDatabaseName,
                 queryExpression,
@@ -160,36 +335,99 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> removeByUri(
-            @NonNull RemoveByUriRequest request) {
+    public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
-            AppSearchBatchResult.Builder<String, Void> resultBuilder =
-                    new AppSearchBatchResult.Builder<>();
-            for (String uri : request.getUris()) {
-                try {
-                    mAppSearchImpl.remove(mPackageName, mDatabaseName, request.getNamespace(), uri);
-                    resultBuilder.setSuccess(uri, /*result=*/null);
-                } catch (Throwable t) {
-                    resultBuilder.setResult(uri, throwableToFailedResult(t));
-                }
-            }
+            mAppSearchImpl.reportUsage(
+                    mPackageName,
+                    mDatabaseName,
+                    request.getNamespace(),
+                    request.getDocumentId(),
+                    request.getUsageTimestampMillis(),
+                    /*systemUsage=*/ false);
             mIsMutated = true;
-            return resultBuilder.build();
+            return null;
         });
     }
 
     @Override
     @NonNull
-    public ListenableFuture<Void> removeByQuery(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest request) {
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+        ListenableFuture<AppSearchBatchResult<String, Void>> future = execute(() -> {
+            AppSearchBatchResult.Builder<String, Void> resultBuilder =
+                    new AppSearchBatchResult.Builder<>();
+            for (String id : request.getIds()) {
+                RemoveStats.Builder removeStatsBuilder = null;
+                if (mLogger != null) {
+                    removeStatsBuilder = new RemoveStats.Builder(mPackageName, mDatabaseName);
+                }
+
+                try {
+                    mAppSearchImpl.remove(mPackageName, mDatabaseName, request.getNamespace(), id,
+                            removeStatsBuilder);
+                    resultBuilder.setSuccess(id, /*value=*/null);
+                } catch (Throwable t) {
+                    resultBuilder.setResult(id, throwableToFailedResult(t));
+                } finally {
+                    if (mLogger != null) {
+                        mLogger.logStats(removeStatsBuilder.build());
+                    }
+                }
+            }
+            // Now that the batch has been written. Persist the newly written data.
+            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+            mIsMutated = true;
+            return resultBuilder.build();
+        });
+        checkForOptimize(/*mutateBatchSize=*/ request.getIds().size());
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<Void> remove(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return execute(() -> {
-            mAppSearchImpl.removeByQuery(mPackageName, mDatabaseName, queryExpression, searchSpec);
+        ListenableFuture<Void> future = execute(() -> {
+            RemoveStats.Builder removeStatsBuilder = null;
+            if (mLogger != null) {
+                removeStatsBuilder = new RemoveStats.Builder(mPackageName, mDatabaseName);
+            }
+
+            mAppSearchImpl.removeByQuery(mPackageName, mDatabaseName, queryExpression,
+                    searchSpec, removeStatsBuilder);
+            // Now that the batch has been written. Persist the newly written data.
+            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
             mIsMutated = true;
+
+            if (mLogger != null) {
+                mLogger.logStats(removeStatsBuilder.build());
+            }
+
+            return null;
+        });
+        checkForOptimize();
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<StorageInfo> getStorageInfo() {
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+        return execute(() -> mAppSearchImpl.getStorageInfoForDatabase(mPackageName, mDatabaseName));
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> requestFlush() {
+        return execute(() -> {
+            mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
             return null;
         });
     }
@@ -199,8 +437,8 @@
     public void close() {
         if (mIsMutated && !mIsClosed) {
             // No future is needed here since the method is void.
-            FutureUtil.execute(mExecutorService, () -> {
-                mAppSearchImpl.persistToDisk();
+            FutureUtil.execute(mExecutor, () -> {
+                mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
                 mIsClosed = true;
                 return null;
             });
@@ -208,6 +446,88 @@
     }
 
     private <T> ListenableFuture<T> execute(Callable<T> callable) {
-        return FutureUtil.execute(mExecutorService, callable);
+        return FutureUtil.execute(mExecutor, callable);
+    }
+
+    /**
+     * Set schema to Icing for no-migration scenario.
+     *
+     * <p>We only need one time {@link #setSchema} call for no-migration scenario by using the
+     * forceoverride in the request.
+     */
+    private SetSchemaResponse setSchemaNoMigrations(@NonNull SetSchemaRequest request,
+            @NonNull Map<String, List<PackageIdentifier>> copySchemasVisibleToPackages,
+            SetSchemaStats.Builder setSchemaStatsBuilder)
+            throws AppSearchException {
+        SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema(
+                mPackageName,
+                mDatabaseName,
+                new ArrayList<>(request.getSchemas()),
+                /*visibilityStore=*/ null,
+                new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
+                copySchemasVisibleToPackages,
+                request.isForceOverride(),
+                request.getVersion(),
+                setSchemaStatsBuilder);
+        if (!request.isForceOverride()) {
+            // check both deleted types and incompatible types are empty. That's the only case we
+            // swallowed in the AppSearchImpl#setSchema().
+            SchemaMigrationUtil.checkDeletedAndIncompatible(setSchemaResponse.getDeletedTypes(),
+                    setSchemaResponse.getIncompatibleTypes());
+        }
+        mIsMutated = true;
+        return setSchemaResponse;
+    }
+
+    private void checkForOptimize(int mutateBatchSize) {
+        mExecutor.execute(() -> {
+            long totalLatencyStartMillis = SystemClock.elapsedRealtime();
+            OptimizeStats.Builder builder = null;
+            try {
+                if (mLogger != null) {
+                    builder = new OptimizeStats.Builder();
+                }
+                mAppSearchImpl.checkForOptimize(mutateBatchSize, builder);
+            } catch (AppSearchException e) {
+                Log.w(TAG, "Error occurred when check for optimize", e);
+            } finally {
+                if (builder != null) {
+                    OptimizeStats oStats = builder
+                            .setTotalLatencyMillis(
+                                    (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis))
+                            .build();
+                    if (mLogger != null && oStats.getOriginalDocumentCount() > 0) {
+                        // see if optimize has been run by checking originalDocumentCount
+                        mLogger.logStats(oStats);
+                    }
+                }
+            }
+        });
+    }
+
+    private void checkForOptimize() {
+        mExecutor.execute(() -> {
+            long totalLatencyStartMillis = SystemClock.elapsedRealtime();
+            OptimizeStats.Builder builder = null;
+            try {
+                if (mLogger != null) {
+                    builder = new OptimizeStats.Builder();
+                }
+                mAppSearchImpl.checkForOptimize(builder);
+            } catch (AppSearchException e) {
+                Log.w(TAG, "Error occurred when check for optimize", e);
+            } finally {
+                if (builder != null) {
+                    OptimizeStats oStats = builder
+                            .setTotalLatencyMillis(
+                                    (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis))
+                            .build();
+                    if (mLogger != null && oStats.getOriginalDocumentCount() > 0) {
+                        // see if optimize has been run by checking originalDocumentCount
+                        mLogger.logStats(oStats);
+                    }
+                }
+            }
+        });
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/UnlimitedLimitConfig.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/UnlimitedLimitConfig.java
new file mode 100644
index 0000000..f1e99cb
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/UnlimitedLimitConfig.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * In Jetpack, AppSearch doesn't enforce artificial limits on number of documents or size of
+ * documents, since the app is the only user of the Icing instance. Icing still enforces a docid
+ * limit of 1M docs.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class UnlimitedLimitConfig implements LimitConfig {
+    @Override
+    public int getMaxDocumentSizeBytes() {
+        return Integer.MAX_VALUE;
+    }
+
+    @Override
+    public int getMaxDocumentCount() {
+        return Integer.MAX_VALUE;
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
deleted file mode 100644
index 186cf93..0000000
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * 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 androidx.appsearch.localstorage;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.appsearch.app.AppSearchResult;
-import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.exceptions.AppSearchException;
-import androidx.collection.ArrayMap;
-import androidx.collection.ArraySet;
-import androidx.core.util.Preconditions;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Manages any visibility settings for all the databases that AppSearchImpl knows about. Persists
- * the visibility settings and reloads them on initialization.
- *
- * <p>The VisibilityStore creates a document for each database. This document holds the visibility
- * settings that apply to that database. The VisibilityStore also creates a schema for these
- * documents and has its own database so that its data doesn't interfere with any clients' data.
- * It persists the document and schema through AppSearchImpl.
- *
- * <p>These visibility settings are used to ensure AppSearch queries respect the clients'
- * settings on who their data is visible to.
- *
- * <p>This class doesn't handle any locking itself. Its callers should handle the locking at a
- * higher level.
- *
- * <p>NOTE: This class holds an instance of AppSearchImpl and AppSearchImpl holds an instance of
- * this class. Take care to not cause any circular dependencies.
- */
-class VisibilityStore {
-    /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
-    @VisibleForTesting
-    static final String SCHEMA_TYPE = "Visibility";
-
-    /**
-     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
-     */
-    @VisibleForTesting
-    static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable";
-
-    /** Schema for the VisibilityStore's docuemnts. */
-    @VisibleForTesting
-    static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
-            .addProperty(new AppSearchSchema.PropertyConfig.Builder(
-                    NOT_PLATFORM_SURFACEABLE_PROPERTY)
-                    .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
-                    .setCardinality(
-                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                    .build())
-            .build();
-
-    /**
-     * These cannot have any of the special characters used by AppSearchImpl (e.g.
-     * {@link AppSearchImpl#PACKAGE_DELIMITER} or {@link AppSearchImpl#DATABASE_DELIMITER}.
-     */
-    static final String PACKAGE_NAME = "VS#Pkg";
-    static final String DATABASE_NAME = "VS#Db";
-
-    /**
-     * Prefix that AppSearchImpl creates for the VisibilityStore based on our package name and
-     * database name. Tracked here to tell when we're looking at our own prefix when looking
-     * through AppSearchImpl.
-     */
-    private static final String VISIBILITY_STORE_PREFIX = AppSearchImpl.createPrefix(PACKAGE_NAME,
-            DATABASE_NAME);
-
-    /** Namespace of documents that contain visibility settings */
-    private static final String NAMESPACE = GenericDocument.DEFAULT_NAMESPACE;
-
-    /**
-     * Prefix to add to all visibility document uri's. IcingSearchEngine doesn't allow empty
-     * uri's.
-     */
-    private static final String URI_PREFIX = "uri:";
-
-    private final AppSearchImpl mAppSearchImpl;
-
-    /**
-     * Maps prefixes to the set of schemas that are platform-hidden within that prefix. All schemas
-     * in the map are prefixed.
-     */
-    private final Map<String, Set<String>> mNotPlatformSurfaceableMap = new ArrayMap<>();
-
-    /**
-     * Creates an uninitialized VisibilityStore object. Callers must also call {@link #initialize()}
-     * before using the object.
-     *
-     * @param appSearchImpl AppSearchImpl instance
-     */
-    VisibilityStore(@NonNull AppSearchImpl appSearchImpl) {
-        mAppSearchImpl = appSearchImpl;
-    }
-
-    /**
-     * Initializes schemas and member variables to track visibility settings.
-     *
-     * <p>This is kept separate from the constructor because this will call methods on
-     * AppSearchImpl. Some may even then recursively call back into VisibilityStore (for example,
-     * {@link AppSearchImpl#setSchema} will call {@link #setVisibility(String, Set)}. We need to
-     * have both
-     * AppSearchImpl and VisibilityStore fully initialized for this call flow to work.
-     *
-     * @throws AppSearchException AppSearchException on AppSearchImpl error.
-     */
-    public void initialize() throws AppSearchException {
-        if (!mAppSearchImpl.hasSchemaTypeLocked(PACKAGE_NAME, DATABASE_NAME, SCHEMA_TYPE)) {
-            // Schema type doesn't exist yet. Add it.
-            mAppSearchImpl.setSchema(PACKAGE_NAME, DATABASE_NAME,
-                    Collections.singletonList(SCHEMA),
-                    /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
-                    /*forceOverride=*/ false);
-        }
-
-        // Populate visibility settings set
-        mNotPlatformSurfaceableMap.clear();
-        for (String prefix : mAppSearchImpl.getPrefixesLocked()) {
-            if (prefix.equals(VISIBILITY_STORE_PREFIX)) {
-                // Our own prefix. Skip
-                continue;
-            }
-
-            try {
-                // Note: We use the other clients' prefixed names as uris
-                GenericDocument document = mAppSearchImpl.getDocument(
-                        PACKAGE_NAME, DATABASE_NAME, NAMESPACE, /*uri=*/ addUriPrefix(prefix));
-
-                String[] schemas = document.getPropertyStringArray(
-                        NOT_PLATFORM_SURFACEABLE_PROPERTY);
-                mNotPlatformSurfaceableMap.put(prefix,
-                        new ArraySet<>(Arrays.asList(schemas)));
-            } catch (AppSearchException e) {
-                if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
-                    // TODO(b/172068212): This indicates some desync error. We were expecting a
-                    //  document, but didn't find one. Should probably reset AppSearch instead of
-                    //  ignoring it.
-                    continue;
-                }
-                // Otherwise, this is some other error we should pass up.
-                throw e;
-            }
-        }
-    }
-
-    /**
-     * Sets visibility settings for {@code prefix}. Any previous visibility settings will be
-     * overwritten.
-     *
-     * @param prefix                        Prefix that identifies who owns the {@code
-     *                                      schemasNotPlatformSurfaceable}.
-     * @param schemasNotPlatformSurfaceable Set of prefixed schemas that should be
-     *                                      hidden from the platform.
-     * @throws AppSearchException on AppSearchImpl error.
-     */
-    public void setVisibility(@NonNull String prefix,
-            @NonNull Set<String> schemasNotPlatformSurfaceable) throws AppSearchException {
-        Preconditions.checkNotNull(prefix);
-        Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
-
-        // Persist the document
-        GenericDocument.Builder visibilityDocument = new GenericDocument.Builder(
-                /*uri=*/ addUriPrefix(prefix), SCHEMA_TYPE)
-                .setNamespace(NAMESPACE);
-        if (!schemasNotPlatformSurfaceable.isEmpty()) {
-            visibilityDocument.setPropertyString(NOT_PLATFORM_SURFACEABLE_PROPERTY,
-                    schemasNotPlatformSurfaceable.toArray(new String[0]));
-        }
-        mAppSearchImpl.putDocument(PACKAGE_NAME, DATABASE_NAME, visibilityDocument.build());
-
-        // Update derived data structures.
-        mNotPlatformSurfaceableMap.put(prefix, schemasNotPlatformSurfaceable);
-    }
-
-    /** Returns if the schema is surfaceable by the platform. */
-    @NonNull
-    public boolean isSchemaPlatformSurfaceable(@NonNull String prefix,
-            @NonNull String prefixedSchema) {
-        Preconditions.checkNotNull(prefix);
-        Preconditions.checkNotNull(prefixedSchema);
-        Set<String> notPlatformSurfaceableSchemas = mNotPlatformSurfaceableMap.get(prefix);
-        if (notPlatformSurfaceableSchemas == null) {
-            return true;
-        }
-        return !notPlatformSurfaceableSchemas.contains(prefixedSchema);
-    }
-
-    /**
-     * Handles an {@code AppSearchImpl#reset()} by clearing any cached state.
-     *
-     * <p> {@link #initialize()} must be called after this.
-     */
-    void handleReset() {
-        mNotPlatformSurfaceableMap.clear();
-    }
-
-    /**
-     * Adds a uri prefix to create a visibility store document's uri.
-     *
-     * @param uri Non-prefixed uri
-     * @return Prefixed uri
-     */
-    private static String addUriPrefix(String uri) {
-        return URI_PREFIX + uri;
-    }
-}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index c598045..eb5a117 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -18,15 +18,18 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.protobuf.ByteString;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Map;
 
 /**
  * Translates a {@link GenericDocument} into a {@link DocumentProto}.
@@ -35,15 +38,25 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class GenericDocumentToProtoConverter {
-    private GenericDocumentToProtoConverter() {}
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+    private static final long[] EMPTY_LONG_ARRAY = new long[0];
+    private static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
+    private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
+    private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0];
+    private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0];
 
-    /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */
+    private GenericDocumentToProtoConverter() {
+    }
+
+    /**
+     * Converts a {@link GenericDocument} into a {@link DocumentProto}.
+     */
     @NonNull
     @SuppressWarnings("unchecked")
     public static DocumentProto toDocumentProto(@NonNull GenericDocument document) {
         Preconditions.checkNotNull(document);
         DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
-        mProtoBuilder.setUri(document.getUri())
+        mProtoBuilder.setUri(document.getId())
                 .setSchema(document.getSchemaType())
                 .setNamespace(document.getNamespace())
                 .setScore(document.getScore())
@@ -96,16 +109,34 @@
         return mProtoBuilder.build();
     }
 
-    /** Converts a {@link DocumentProto} into a {@link GenericDocument}. */
+    /**
+     * Converts a {@link DocumentProto} into a {@link GenericDocument}.
+     *
+     * <p>In the case that the {@link DocumentProto} object proto has no values set, the
+     * converter searches for the matching property name in the {@link SchemaTypeConfigProto}
+     * object for the document, and infers the correct default value to set for the empty
+     * property based on the data type of the property defined by the schema type.
+     *
+     * @param proto         the document to convert to a {@link GenericDocument} instance. The
+     *                      document proto should have its package + database prefix stripped
+     *                      from its fields.
+     * @param prefix        the package + database prefix used searching the {@code schemaTypeMap}.
+     * @param schemaTypeMap map of prefixed schema type to {@link SchemaTypeConfigProto}, used
+     *                      for looking up the default empty value to set for a document property
+     *                      that has all empty values.
+     */
     @NonNull
-    public static GenericDocument toGenericDocument(@NonNull DocumentProto proto) {
+    public static GenericDocument toGenericDocument(@NonNull DocumentProto proto,
+            @NonNull String prefix,
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) {
         Preconditions.checkNotNull(proto);
         GenericDocument.Builder<?> documentBuilder =
-                new GenericDocument.Builder<>(proto.getUri(), proto.getSchema())
-                        .setNamespace(proto.getNamespace())
+                new GenericDocument.Builder<>(proto.getNamespace(), proto.getUri(),
+                        proto.getSchema())
                         .setScore(proto.getScore())
                         .setTtlMillis(proto.getTtlMs())
                         .setCreationTimestampMillis(proto.getCreationTimestampMs());
+        String prefixedSchemaType = prefix + proto.getSchema();
 
         for (int i = 0; i < proto.getPropertiesCount(); i++) {
             PropertyProto property = proto.getProperties(i);
@@ -143,13 +174,51 @@
             } else if (property.getDocumentValuesCount() > 0) {
                 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
                 for (int j = 0; j < values.length; j++) {
-                    values[j] = toGenericDocument(property.getDocumentValues(j));
+                    values[j] = toGenericDocument(property.getDocumentValues(j), prefix,
+                            schemaTypeMap);
                 }
                 documentBuilder.setPropertyDocument(name, values);
             } else {
-                throw new IllegalStateException("Unknown type of value: " + name);
+                // TODO(b/184966497): Optimize by caching PropertyConfigProto
+                setEmptyProperty(name, documentBuilder,
+                        schemaTypeMap.get(prefixedSchemaType));
             }
         }
         return documentBuilder.build();
     }
+
+    private static void setEmptyProperty(@NonNull String propertyName,
+            @NonNull GenericDocument.Builder<?> documentBuilder,
+            @NonNull SchemaTypeConfigProto schema) {
+        @AppSearchSchema.PropertyConfig.DataType int dataType = 0;
+        for (int i = 0; i < schema.getPropertiesCount(); ++i) {
+            if (propertyName.equals(schema.getProperties(i).getPropertyName())) {
+                dataType = schema.getProperties(i).getDataType().getNumber();
+                break;
+            }
+        }
+
+        switch (dataType) {
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING:
+                documentBuilder.setPropertyString(propertyName, EMPTY_STRING_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG:
+                documentBuilder.setPropertyLong(propertyName, EMPTY_LONG_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE:
+                documentBuilder.setPropertyDouble(propertyName, EMPTY_DOUBLE_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN:
+                documentBuilder.setPropertyBoolean(propertyName, EMPTY_BOOLEAN_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES:
+                documentBuilder.setPropertyBytes(propertyName, EMPTY_BYTES_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
+                documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY);
+                break;
+            default:
+                throw new IllegalStateException("Unknown type of value: " + propertyName);
+        }
+    }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
new file mode 100644
index 0000000..e04d756
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.converter;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+
+import com.google.android.icing.proto.StatusProto;
+
+/**
+ * Translates an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ResultCodeToProtoConverter {
+
+    private static final String TAG = "AppSearchResultCodeToPr";
+    private ResultCodeToProtoConverter() {}
+
+    /** Converts an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}. */
+    public static @AppSearchResult.ResultCode int toResultCode(
+            @NonNull StatusProto.Code statusCode) {
+        switch (statusCode) {
+            case OK:
+                return AppSearchResult.RESULT_OK;
+            case OUT_OF_SPACE:
+                return AppSearchResult.RESULT_OUT_OF_SPACE;
+            case INTERNAL:
+                return AppSearchResult.RESULT_INTERNAL_ERROR;
+            case UNKNOWN:
+                return AppSearchResult.RESULT_UNKNOWN_ERROR;
+            case NOT_FOUND:
+                return AppSearchResult.RESULT_NOT_FOUND;
+            case INVALID_ARGUMENT:
+                return AppSearchResult.RESULT_INVALID_ARGUMENT;
+            default:
+                // Some unknown/unsupported error
+                Log.e(TAG, "Cannot convert IcingSearchEngine status code: "
+                        + statusCode + " to AppSearchResultCode.");
+                return AppSearchResult.RESULT_INTERNAL_ERROR;
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index f52c220..4e3e3a9 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -23,6 +23,7 @@
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.DocumentIndexingConfig;
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProtoOrBuilder;
@@ -46,10 +47,12 @@
      * {@link SchemaTypeConfigProto}.
      */
     @NonNull
-    public static SchemaTypeConfigProto toSchemaTypeConfigProto(@NonNull AppSearchSchema schema) {
+    public static SchemaTypeConfigProto toSchemaTypeConfigProto(@NonNull AppSearchSchema schema,
+            int version) {
         Preconditions.checkNotNull(schema);
-        SchemaTypeConfigProto.Builder protoBuilder =
-                SchemaTypeConfigProto.newBuilder().setSchemaType(schema.getSchemaType());
+        SchemaTypeConfigProto.Builder protoBuilder = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType(schema.getSchemaType())
+                .setVersion(version);
         List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
             PropertyConfigProto propertyProto = toPropertyConfigProto(properties.get(i));
@@ -64,7 +67,6 @@
         Preconditions.checkNotNull(property);
         PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
                 .setPropertyName(property.getName());
-        StringIndexingConfig.Builder indexingConfig = StringIndexingConfig.newBuilder();
 
         // Set dataType
         @AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType();
@@ -75,12 +77,6 @@
         }
         builder.setDataType(dataTypeProto);
 
-        // Set schemaType
-        String schemaType = property.getSchemaType();
-        if (schemaType != null) {
-            builder.setSchemaType(schemaType);
-        }
-
         // Set cardinality
         @AppSearchSchema.PropertyConfig.Cardinality int cardinality = property.getCardinality();
         PropertyConfigProto.Cardinality.Code cardinalityProto =
@@ -90,36 +86,25 @@
         }
         builder.setCardinality(cardinalityProto);
 
-        // Set indexingType
-        @AppSearchSchema.PropertyConfig.IndexingType int indexingType = property.getIndexingType();
-        TermMatchType.Code termMatchTypeProto;
-        switch (indexingType) {
-            case AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE:
-                termMatchTypeProto = TermMatchType.Code.UNKNOWN;
-                break;
-            case AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS:
-                termMatchTypeProto = TermMatchType.Code.EXACT_ONLY;
-                break;
-            case AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES:
-                termMatchTypeProto = TermMatchType.Code.PREFIX;
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
-        }
-        indexingConfig.setTermMatchType(termMatchTypeProto);
+        if (property instanceof AppSearchSchema.StringPropertyConfig) {
+            AppSearchSchema.StringPropertyConfig stringProperty =
+                    (AppSearchSchema.StringPropertyConfig) property;
+            StringIndexingConfig stringIndexingConfig = StringIndexingConfig.newBuilder()
+                    .setTermMatchType(convertTermMatchTypeToProto(stringProperty.getIndexingType()))
+                    .setTokenizerType(
+                            convertTokenizerTypeToProto(stringProperty.getTokenizerType()))
+                    .build();
+            builder.setStringIndexingConfig(stringIndexingConfig);
 
-        // Set tokenizerType
-        @AppSearchSchema.PropertyConfig.TokenizerType int tokenizerType =
-                property.getTokenizerType();
-        StringIndexingConfig.TokenizerType.Code tokenizerTypeProto =
-                StringIndexingConfig.TokenizerType.Code.forNumber(tokenizerType);
-        if (tokenizerTypeProto == null) {
-            throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType);
+        } else if (property instanceof AppSearchSchema.DocumentPropertyConfig) {
+            AppSearchSchema.DocumentPropertyConfig documentProperty =
+                    (AppSearchSchema.DocumentPropertyConfig) property;
+            builder
+                    .setSchemaType(documentProperty.getSchemaType())
+                    .setDocumentIndexingConfig(
+                            DocumentIndexingConfig.newBuilder().setIndexNestedProperties(
+                                    documentProperty.shouldIndexNestedProperties()));
         }
-        indexingConfig.setTokenizerType(tokenizerTypeProto);
-
-        // Build!
-        builder.setStringIndexingConfig(indexingConfig);
         return builder.build();
     }
 
@@ -130,7 +115,8 @@
     @NonNull
     public static AppSearchSchema toAppSearchSchema(@NonNull SchemaTypeConfigProtoOrBuilder proto) {
         Preconditions.checkNotNull(proto);
-        AppSearchSchema.Builder builder = new AppSearchSchema.Builder(proto.getSchemaType());
+        AppSearchSchema.Builder builder =
+                new AppSearchSchema.Builder(proto.getSchemaType());
         List<PropertyConfigProto> properties = proto.getPropertiesList();
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
@@ -143,39 +129,99 @@
     private static AppSearchSchema.PropertyConfig toPropertyConfig(
             @NonNull PropertyConfigProto proto) {
         Preconditions.checkNotNull(proto);
-        AppSearchSchema.PropertyConfig.Builder builder =
-                new AppSearchSchema.PropertyConfig.Builder(proto.getPropertyName())
-                        .setDataType(proto.getDataType().getNumber())
+        switch (proto.getDataType()) {
+            case STRING:
+                return toStringPropertyConfig(proto);
+            case INT64:
+                return new AppSearchSchema.LongPropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case DOUBLE:
+                return new AppSearchSchema.DoublePropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case BOOLEAN:
+                return new AppSearchSchema.BooleanPropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case BYTES:
+                return new AppSearchSchema.BytesPropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case DOCUMENT:
+                return toDocumentPropertyConfig(proto);
+            default:
+                throw new IllegalArgumentException("Invalid dataType: " + proto.getDataType());
+        }
+    }
+
+    @NonNull
+    private static AppSearchSchema.StringPropertyConfig toStringPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        AppSearchSchema.StringPropertyConfig.Builder builder =
+                new AppSearchSchema.StringPropertyConfig.Builder(proto.getPropertyName())
                         .setCardinality(proto.getCardinality().getNumber())
                         .setTokenizerType(
                                 proto.getStringIndexingConfig().getTokenizerType().getNumber());
 
-        // Set schema
-        if (!proto.getSchemaType().isEmpty()) {
-            builder.setSchemaType(proto.getSchemaType());
-        }
-
         // Set indexingType
-        @AppSearchSchema.PropertyConfig.IndexingType int indexingType;
         TermMatchType.Code termMatchTypeProto = proto.getStringIndexingConfig().getTermMatchType();
-        switch (termMatchTypeProto) {
+        builder.setIndexingType(convertTermMatchTypeFromProto(termMatchTypeProto));
+
+        return builder.build();
+    }
+
+    @NonNull
+    private static AppSearchSchema.DocumentPropertyConfig toDocumentPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        return new AppSearchSchema.DocumentPropertyConfig.Builder(
+                proto.getPropertyName(), proto.getSchemaType())
+                .setCardinality(proto.getCardinality().getNumber())
+                .setShouldIndexNestedProperties(
+                        proto.getDocumentIndexingConfig().getIndexNestedProperties())
+                .build();
+    }
+
+    @NonNull
+    private static TermMatchType.Code convertTermMatchTypeToProto(
+            @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType) {
+        switch (indexingType) {
+            case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE:
+                return TermMatchType.Code.UNKNOWN;
+            case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS:
+                return TermMatchType.Code.EXACT_ONLY;
+            case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES:
+                return TermMatchType.Code.PREFIX;
+            default:
+                throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
+        }
+    }
+
+    @AppSearchSchema.StringPropertyConfig.IndexingType
+    private static int convertTermMatchTypeFromProto(@NonNull TermMatchType.Code termMatchType) {
+        switch (termMatchType) {
             case UNKNOWN:
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
-                break;
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
             case EXACT_ONLY:
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS;
-                break;
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS;
             case PREFIX:
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-                break;
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
             default:
                 // Avoid crashing in the 'read' path; we should try to interpret the document to the
                 // extent possible.
-                Log.w(TAG, "Invalid indexingType: " + termMatchTypeProto.getNumber());
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+                Log.w(TAG, "Invalid indexingType: " + termMatchType.getNumber());
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
         }
-        builder.setIndexingType(indexingType);
+    }
 
-        return builder.build();
+    @NonNull
+    private static StringIndexingConfig.TokenizerType.Code convertTokenizerTypeToProto(
+            @AppSearchSchema.StringPropertyConfig.TokenizerType int tokenizerType) {
+        StringIndexingConfig.TokenizerType.Code tokenizerTypeProto =
+                StringIndexingConfig.TokenizerType.Code.forNumber(tokenizerType);
+        if (tokenizerTypeProto == null) {
+            throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType);
+        }
+        return tokenizerTypeProto;
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index d53492e..b3da971 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.localstorage.converter;
 
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+
 import android.os.Bundle;
 
 import androidx.annotation.NonNull;
@@ -25,6 +27,7 @@
 import androidx.appsearch.app.SearchResultPage;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SearchResultProto;
 import com.google.android.icing.proto.SearchResultProtoOrBuilder;
 import com.google.android.icing.proto.SnippetMatchProto;
@@ -32,6 +35,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Translates a {@link SearchResultProto} into {@link SearchResult}s.
@@ -46,23 +50,34 @@
     /**
      * Translate a {@link SearchResultProto} into {@link SearchResultPage}.
      *
-     * @param proto The {@link SearchResultProto} containing results.
-     * @param packageNames A parallel array of package names. The package name at index 'i' of
-     *                     this list should be the package that indexed the document at index 'i'
-     *                     of proto.getResults(i).
+     * @param proto         The {@link SearchResultProto} containing results.
+     * @param packageNames  A parallel array of package names. The package name at index 'i' of
+     *                      this list should be the package that indexed the document at index 'i'
+     *                      of proto.getResults(i).
+     * @param databaseNames A parallel array of database names. The database name at index 'i' of
+     *                      this list shold be the database that indexed the document at index 'i'
+     *                      of proto.getResults(i).
+     * @param schemaMap     A map of prefixes to an inner-map of prefixed schema type to
+     *                      SchemaTypeConfigProtos, used for setting a default value for results
+     *                      with DocumentProtos that have empty values.
      * @return {@link SearchResultPage} of results.
      */
     @NonNull
     public static SearchResultPage toSearchResultPage(@NonNull SearchResultProtoOrBuilder proto,
-            @NonNull List<String> packageNames) {
-        Preconditions.checkArgument(proto.getResultsCount() == packageNames.size(), "Size of "
-                + "results does not match the number of package names.");
+            @NonNull List<String> packageNames, @NonNull List<String> databaseNames,
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+        Preconditions.checkArgument(
+                proto.getResultsCount() == packageNames.size(),
+                "Size of results does not match the number of package names.");
         Bundle bundle = new Bundle();
         bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
         ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
-            resultBundles.add(toSearchResultBundle(proto.getResults(i),
-                    packageNames.get(i)));
+            String prefix = createPrefix(packageNames.get(i), databaseNames.get(i));
+            Map<String, SchemaTypeConfigProto> schemaTypeMap = schemaMap.get(prefix);
+            SearchResult result = toSearchResult(
+                    proto.getResults(i), packageNames.get(i), databaseNames.get(i), schemaTypeMap);
+            resultBundles.add(result.getBundle());
         }
         bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
         return new SearchResultPage(bundle);
@@ -71,53 +86,53 @@
     /**
      * Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}.
      *
-     * @param proto The proto to be converted.
-     * @param packageName The package name associated with the document in {@code proto}.
+     * @param proto                The proto to be converted.
+     * @param packageName          The package name associated with the document in {@code proto}.
+     * @param databaseName         The database name associated with the document in {@code proto}.
+     * @param schemaTypeToProtoMap A map of prefixed schema types to their corresponding
+     *                             SchemaTypeConfigProto, used for setting a default value for
+     *                             results with DocumentProtos that have empty values.
      * @return A {@link SearchResult} bundle.
      */
     @NonNull
-    private static Bundle toSearchResultBundle(
-            @NonNull SearchResultProto.ResultProtoOrBuilder proto, @NonNull String packageName) {
-        Bundle bundle = new Bundle();
+    private static SearchResult toSearchResult(
+            @NonNull SearchResultProto.ResultProtoOrBuilder proto,
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeToProtoMap) {
+        String prefix = createPrefix(packageName, databaseName);
         GenericDocument document =
-                GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument());
-        bundle.putBundle(SearchResult.DOCUMENT_FIELD, document.getBundle());
-        bundle.putString(SearchResult.PACKAGE_NAME_FIELD, packageName);
-
-        ArrayList<Bundle> matchList = new ArrayList<>();
+                GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument(), prefix,
+                        schemaTypeToProtoMap);
+        SearchResult.Builder builder =
+                new SearchResult.Builder(packageName, databaseName)
+                        .setGenericDocument(document).setRankingSignal(proto.getScore());
         if (proto.hasSnippet()) {
             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(
+                    SearchResult.MatchInfo matchInfo = toMatchInfo(
                             entry.getSnippetMatches(j), entry.getPropertyName());
-                    matchList.add(matchInfoBundle);
+                    builder.addMatchInfo(matchInfo);
                 }
             }
         }
-        bundle.putParcelableArrayList(SearchResult.MATCHES_FIELD, matchList);
-
-        return bundle;
+        return builder.build();
     }
 
-    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;
+    private static SearchResult.MatchInfo toMatchInfo(
+            @NonNull SnippetMatchProto snippetMatchProto, @NonNull String propertyPath) {
+        return new SearchResult.MatchInfo.Builder(propertyPath)
+                .setExactMatchRange(
+                        new SearchResult.MatchRange(
+                                snippetMatchProto.getExactMatchUtf16Position(),
+                                snippetMatchProto.getExactMatchUtf16Position()
+                                        + snippetMatchProto.getExactMatchUtf16Length()))
+                .setSnippetRange(
+                        new SearchResult.MatchRange(
+                                snippetMatchProto.getWindowUtf16Position(),
+                                snippetMatchProto.getWindowUtf16Position()
+                                        + snippetMatchProto.getWindowUtf16Length()))
+                .build();
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index 485d361..abc97f3 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -25,10 +25,6 @@
 import com.google.android.icing.proto.ScoringSpecProto;
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.TermMatchType;
-import com.google.android.icing.proto.TypePropertyMask;
-
-import java.util.List;
-import java.util.Map;
 
 /**
  * Translates a {@link SearchSpec} into icing search protos.
@@ -45,8 +41,8 @@
     public static SearchSpecProto toSearchSpecProto(@NonNull SearchSpec spec) {
         Preconditions.checkNotNull(spec);
         SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder()
-                .addAllSchemaTypeFilters(spec.getSchemaTypes())
-                .addAllNamespaceFilters(spec.getNamespaces());
+                .addAllSchemaTypeFilters(spec.getFilterSchemas())
+                .addAllNamespaceFilters(spec.getFilterNamespaces());
 
         @SearchSpec.TermMatch int termMatchCode = spec.getTermMatch();
         TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode);
@@ -62,20 +58,15 @@
     @NonNull
     public static ResultSpecProto toResultSpecProto(@NonNull SearchSpec spec) {
         Preconditions.checkNotNull(spec);
-        ResultSpecProto.Builder builder = ResultSpecProto.newBuilder()
+        return ResultSpecProto.newBuilder()
                 .setNumPerPage(spec.getResultCountPerPage())
                 .setSnippetSpec(
                         ResultSpecProto.SnippetSpecProto.newBuilder()
                                 .setNumToSnippet(spec.getSnippetCount())
                                 .setNumMatchesPerProperty(spec.getSnippetCountPerProperty())
-                                .setMaxWindowBytes(spec.getMaxSnippetSize()));
-        Map<String, List<String>> projectionTypePropertyPaths = spec.getProjections();
-        for (Map.Entry<String, List<String>> e : projectionTypePropertyPaths.entrySet()) {
-            builder.addTypePropertyMasks(
-                    TypePropertyMask.newBuilder().setSchemaType(
-                            e.getKey()).addAllPaths(e.getValue()));
-        }
-        return builder.build();
+                                .setMaxWindowBytes(spec.getMaxSnippetSize()))
+                .addAllTypePropertyMasks(TypePropertyPathToProtoConverter.toTypePropertyMaskList(
+                        spec.getProjections())).build();
     }
 
     /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */
@@ -107,6 +98,14 @@
                 return ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP;
             case SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE:
                 return ScoringSpecProto.RankingStrategy.Code.RELEVANCE_SCORE;
+            case SearchSpec.RANKING_STRATEGY_USAGE_COUNT:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_COUNT;
+            case SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_LAST_USED_TIMESTAMP;
+            case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_COUNT;
+            case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_LAST_USED_TIMESTAMP;
             default:
                 throw new IllegalArgumentException("Invalid result ranking strategy: "
                         + rankingStrategyCode);
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SetSchemaResponseToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SetSchemaResponseToProtoConverter.java
new file mode 100644
index 0000000..62d788c
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SetSchemaResponseToProtoConverter.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.converter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.SetSchemaResultProto;
+
+/**
+ * Translates a {@link SetSchemaResultProto} into {@link SetSchemaResponse}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SetSchemaResponseToProtoConverter {
+
+    private SetSchemaResponseToProtoConverter() {}
+
+    /**
+     * Translate a {@link SetSchemaResultProto} into {@link SetSchemaResponse}.
+     *
+     * @param proto  The {@link SetSchemaResultProto} containing results.
+     * @param prefix The prefix need to removed from schemaTypes
+     * @return The {@link SetSchemaResponse} object.
+     */
+    @NonNull
+    public static SetSchemaResponse toSetSchemaResponse(@NonNull SetSchemaResultProto proto,
+            @NonNull String prefix) {
+        Preconditions.checkNotNull(proto);
+        Preconditions.checkNotNull(prefix);
+        SetSchemaResponse.Builder builder = new SetSchemaResponse.Builder();
+
+        for (int i = 0; i < proto.getDeletedSchemaTypesCount(); i++) {
+            builder.addDeletedType(
+                    proto.getDeletedSchemaTypes(i).substring(prefix.length()));
+        }
+
+        for (int i = 0; i < proto.getIncompatibleSchemaTypesCount(); i++) {
+            builder.addIncompatibleType(
+                    proto.getIncompatibleSchemaTypes(i).substring(prefix.length()));
+        }
+
+        return builder.build();
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
new file mode 100644
index 0000000..98f5642
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.converter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.TypePropertyMask;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates a <code>Map<String, List<String>></code> into <code>List<TypePropertyMask></code>.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class TypePropertyPathToProtoConverter {
+    private TypePropertyPathToProtoConverter() {}
+
+    /** Extracts {@link TypePropertyMask} information from a {@link Map}. */
+    @NonNull
+    public static List<TypePropertyMask> toTypePropertyMaskList(@NonNull Map<String,
+            List<String>> typePropertyPaths) {
+        Preconditions.checkNotNull(typePropertyPaths);
+        List<TypePropertyMask> typePropertyMasks = new ArrayList<>(typePropertyPaths.size());
+        for (Map.Entry<String, List<String>> e : typePropertyPaths.entrySet()) {
+            typePropertyMasks.add(
+                    TypePropertyMask.newBuilder().setSchemaType(
+                            e.getKey()).addAllPaths(e.getValue()).build());
+        }
+        return typePropertyMasks;
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
new file mode 100644
index 0000000..e3abf90
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class for setting basic information to log for all function calls.
+ *
+ * <p>This class can set which stats to log for both batch and non-batch
+ * {@link androidx.appsearch.app.AppSearchSession} calls.
+ *
+ * <p>Some function calls may have their own detailed stats class like {@link PutDocumentStats}.
+ * However, {@link CallStats} can still be used along with the detailed stats class for easy
+ * aggregation/analysis with other function calls.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CallStats {
+    @IntDef(value = {
+            CALL_TYPE_UNKNOWN,
+            CALL_TYPE_INITIALIZE,
+            CALL_TYPE_SET_SCHEMA,
+            CALL_TYPE_PUT_DOCUMENTS,
+            CALL_TYPE_GET_DOCUMENTS,
+            CALL_TYPE_REMOVE_DOCUMENTS_BY_ID,
+            CALL_TYPE_PUT_DOCUMENT,
+            CALL_TYPE_GET_DOCUMENT,
+            CALL_TYPE_REMOVE_DOCUMENT_BY_ID,
+            CALL_TYPE_SEARCH,
+            CALL_TYPE_OPTIMIZE,
+            CALL_TYPE_FLUSH,
+            CALL_TYPE_GLOBAL_SEARCH,
+            CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH,
+            CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CallType {
+    }
+
+    public static final int CALL_TYPE_UNKNOWN = 0;
+    public static final int CALL_TYPE_INITIALIZE = 1;
+    public static final int CALL_TYPE_SET_SCHEMA = 2;
+    public static final int CALL_TYPE_PUT_DOCUMENTS = 3;
+    public static final int CALL_TYPE_GET_DOCUMENTS = 4;
+    public static final int CALL_TYPE_REMOVE_DOCUMENTS_BY_ID = 5;
+    public static final int CALL_TYPE_PUT_DOCUMENT = 6;
+    public static final int CALL_TYPE_GET_DOCUMENT = 7;
+    public static final int CALL_TYPE_REMOVE_DOCUMENT_BY_ID = 8;
+    public static final int CALL_TYPE_SEARCH = 9;
+    public static final int CALL_TYPE_OPTIMIZE = 10;
+    public static final int CALL_TYPE_FLUSH = 11;
+    public static final int CALL_TYPE_GLOBAL_SEARCH = 12;
+    public static final int CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH = 13;
+    public static final int CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH = 14;
+
+    @Nullable
+    private final String mPackageName;
+    @Nullable
+    private final String mDatabase;
+    /**
+     * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+     * internal state.
+     */
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+
+    @CallType
+    private final int mCallType;
+    private final int mEstimatedBinderLatencyMillis;
+    private final int mNumOperationsSucceeded;
+    private final int mNumOperationsFailed;
+
+    CallStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mCallType = builder.mCallType;
+        mEstimatedBinderLatencyMillis = builder.mEstimatedBinderLatencyMillis;
+        mNumOperationsSucceeded = builder.mNumOperationsSucceeded;
+        mNumOperationsFailed = builder.mNumOperationsFailed;
+    }
+
+    /** Returns calling package name. */
+    @Nullable
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns calling database name. */
+    @Nullable
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns status code for this api call. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns total latency of this api call in millis. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /** Returns type of the call. */
+    @CallType
+    public int getCallType() {
+        return mCallType;
+    }
+
+    /** Returns estimated binder latency, in milliseconds */
+    public int getEstimatedBinderLatencyMillis() {
+        return mEstimatedBinderLatencyMillis;
+    }
+
+    /**
+     * Returns number of operations succeeded.
+     *
+     * <p>For example, for
+     * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+     * successful put operations. In this case, how many documents are successfully indexed.
+     *
+     * <p>For non-batch calls such as
+     * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+     * {@link CallStats#getNumOperationsSucceeded()} and
+     * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+     * operation.
+     */
+    public int getNumOperationsSucceeded() {
+        return mNumOperationsSucceeded;
+    }
+
+    /**
+     * Returns number of operations failed.
+     *
+     * <p>For example, for
+     * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+     * failed put operations. In this case, how many documents are failed to be indexed.
+     *
+     * <p>For non-batch calls such as {@link androidx.appsearch.app.AppSearchSession#setSchema},
+     * the sum of {@link CallStats#getNumOperationsSucceeded()} and
+     * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+     * operation.
+     */
+    public int getNumOperationsFailed() {
+        return mNumOperationsFailed;
+    }
+
+    /** Builder for {@link CallStats}. */
+    public static class Builder {
+        @Nullable
+        String mPackageName;
+        @Nullable
+        String mDatabase;
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+        int mTotalLatencyMillis;
+        @CallType
+        int mCallType;
+        int mEstimatedBinderLatencyMillis;
+        int mNumOperationsSucceeded;
+        int mNumOperationsFailed;
+
+        /** Sets the PackageName used by the session. */
+        @NonNull
+        public Builder setPackageName(@NonNull String packageName) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            return this;
+        }
+
+        /** Sets the database used by the session. */
+        @NonNull
+        public Builder setDatabase(@NonNull String database) {
+            mDatabase = Preconditions.checkNotNull(database);
+            return this;
+        }
+
+        /** Sets the status code. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets total latency in millis. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /** Sets type of the call. */
+        @NonNull
+        public Builder setCallType(@CallType int callType) {
+            mCallType = callType;
+            return this;
+        }
+
+        /** Sets estimated binder latency, in milliseconds. */
+        @NonNull
+        public Builder setEstimatedBinderLatencyMillis(int estimatedBinderLatencyMillis) {
+            mEstimatedBinderLatencyMillis = estimatedBinderLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets number of operations succeeded.
+         *
+         * <p>For example, for
+         * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of
+         * individual successful put operations. In this case, how many documents are
+         * successfully indexed.
+         *
+         * <p>For non-batch calls such as
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+         * {@link CallStats#getNumOperationsSucceeded()} and
+         * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+         * operation.
+         */
+        @NonNull
+        public Builder setNumOperationsSucceeded(int numOperationsSucceeded) {
+            mNumOperationsSucceeded = numOperationsSucceeded;
+            return this;
+        }
+
+        /**
+         * Sets number of operations failed.
+         *
+         * <p>For example, for {@link androidx.appsearch.app.AppSearchSession#put}, it is the
+         * total number of individual failed put operations. In this case, how many documents
+         * are failed to be indexed.
+         *
+         * <p>For non-batch calls such as
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+         * {@link CallStats#getNumOperationsSucceeded()} and
+         * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+         * operation.
+         */
+        @NonNull
+        public Builder setNumOperationsFailed(int numOperationsFailed) {
+            mNumOperationsFailed = numOperationsFailed;
+            return this;
+        }
+
+        /** Creates {@link CallStats} object from {@link Builder} instance. */
+        @NonNull
+        public CallStats build() {
+            return new CallStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java
new file mode 100644
index 0000000..f88d257
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class holds detailed stats for initialization
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class InitializeStats {
+    /**
+     * The cause of IcingSearchEngine recovering from a previous bad state during initialization.
+     */
+    @IntDef(value = {
+            // It needs to be sync with RecoveryCause in
+            // external/icing/proto/icing/proto/logging.proto#InitializeStatsProto
+            RECOVERY_CAUSE_NONE,
+            RECOVERY_CAUSE_DATA_LOSS,
+            RECOVERY_CAUSE_INCONSISTENT_WITH_GROUND_TRUTH,
+            RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH,
+            RECOVERY_CAUSE_IO_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RecoveryCause {
+    }
+
+    // No recovery happened.
+    public static final int RECOVERY_CAUSE_NONE = 0;
+    // Data loss in ground truth.
+    public static final int RECOVERY_CAUSE_DATA_LOSS = 1;
+    // Data in index is inconsistent with ground truth.
+    public static final int RECOVERY_CAUSE_INCONSISTENT_WITH_GROUND_TRUTH = 2;
+    // Total checksum of all the components does not match.
+    public static final int RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH = 3;
+    // Random I/O errors.
+    public static final int RECOVERY_CAUSE_IO_ERROR = 4;
+
+    /**
+     * Status regarding how much data is lost during the initialization.
+     */
+    @IntDef(value = {
+            // It needs to be sync with DocumentStoreDataStatus in
+            // external/icing/proto/icing/proto/logging.proto#InitializeStatsProto
+
+            DOCUMENT_STORE_DATA_STATUS_NO_DATA_LOSS,
+            DOCUMENT_STORE_DATA_STATUS_PARTIAL_LOSS,
+            DOCUMENT_STORE_DATA_STATUS_COMPLETE_LOSS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DocumentStoreDataStatus {
+    }
+
+    // Document store is successfully initialized or fully recovered.
+    public static final int DOCUMENT_STORE_DATA_STATUS_NO_DATA_LOSS = 0;
+    // Ground truth data is partially lost.
+    public static final int DOCUMENT_STORE_DATA_STATUS_PARTIAL_LOSS = 1;
+    // Ground truth data is completely lost.
+    public static final int DOCUMENT_STORE_DATA_STATUS_COMPLETE_LOSS = 2;
+
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+    /** Whether the initialize() detects deSync. */
+    private final boolean mHasDeSync;
+    /** Time used to read and process the schema and namespaces. */
+    private final int mPrepareSchemaAndNamespacesLatencyMillis;
+    /** Time used to read and process the visibility store. */
+    private final int mPrepareVisibilityStoreLatencyMillis;
+    /** Overall time used for the native function call. */
+    private final int mNativeLatencyMillis;
+    @RecoveryCause
+    private final int mNativeDocumentStoreRecoveryCause;
+    @RecoveryCause
+    private final int mNativeIndexRestorationCause;
+    @RecoveryCause
+    private final int mNativeSchemaStoreRecoveryCause;
+    /** Time used to recover the document store. */
+    private final int mNativeDocumentStoreRecoveryLatencyMillis;
+    /** Time used to restore the index. */
+    private final int mNativeIndexRestorationLatencyMillis;
+    /** Time used to recover the schema store. */
+    private final int mNativeSchemaStoreRecoveryLatencyMillis;
+    /** Status regarding how much data is lost during the initialization. */
+    private final int mNativeDocumentStoreDataStatus;
+    /**
+     * Returns number of documents currently in document store. Those may include alive, deleted,
+     * and expired documents.
+     */
+    private final int mNativeNumDocuments;
+    /** Returns number of schema types currently in the schema store. */
+    private final int mNativeNumSchemaTypes;
+    /** Whether we had to reset the index, losing all data, during initialization. */
+    private final boolean mHasReset;
+    /** If we had to reset, contains the status code of the reset operation. */
+    @AppSearchResult.ResultCode
+    private final int mResetStatusCode;
+
+    /** Returns the status of the initialization. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns the total latency in milliseconds for the initialization. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /**
+     * Returns whether the initialize() detects deSync.
+     *
+     * <p>If there is a deSync, it means AppSearch and IcingSearchEngine have an inconsistent view
+     * of what data should exist.
+     */
+    public boolean hasDeSync() {
+        return mHasDeSync;
+    }
+
+    /** Returns time used to read and process the schema and namespaces. */
+    public int getPrepareSchemaAndNamespacesLatencyMillis() {
+        return mPrepareSchemaAndNamespacesLatencyMillis;
+    }
+
+    /** Returns time used to read and process the visibility file. */
+    public int getPrepareVisibilityStoreLatencyMillis() {
+        return mPrepareVisibilityStoreLatencyMillis;
+    }
+
+    /** Returns overall time used for the native function call. */
+    public int getNativeLatencyMillis() {
+        return mNativeLatencyMillis;
+    }
+
+    /** Returns recovery cause for document store.
+     *
+     *  <p> Possible recovery causes for document store:
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_DATA_LOSS}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+     */
+    @RecoveryCause
+    public int getDocumentStoreRecoveryCause() {
+        return mNativeDocumentStoreRecoveryCause;
+    }
+
+    /** Returns restoration cause for index store.
+     *
+     *  <p> Possible causes:
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_INCONSISTENT_WITH_GROUND_TRUTH}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+     */
+    @RecoveryCause
+    public int getIndexRestorationCause() {
+        return mNativeIndexRestorationCause;
+    }
+
+    /** Returns recovery cause for schema store.
+     *
+     *  <p> Possible causes:
+     *      <li> IO_ERROR
+     */
+    @RecoveryCause
+    public int getSchemaStoreRecoveryCause() {
+        return mNativeSchemaStoreRecoveryCause;
+    }
+
+    /** Returns time used to recover the document store. */
+    public int getDocumentStoreRecoveryLatencyMillis() {
+        return mNativeDocumentStoreRecoveryLatencyMillis;
+    }
+
+    /** Returns time used to restore the index. */
+    public int getIndexRestorationLatencyMillis() {
+        return mNativeIndexRestorationLatencyMillis;
+    }
+
+    /** Returns time used to recover the schema store. */
+    public int getSchemaStoreRecoveryLatencyMillis() {
+        return mNativeSchemaStoreRecoveryLatencyMillis;
+    }
+
+    /** Returns status about how much data is lost during the initialization. */
+    @DocumentStoreDataStatus
+    public int getDocumentStoreDataStatus() {
+        return mNativeDocumentStoreDataStatus;
+    }
+
+    /**
+     * Returns number of documents currently in document store. Those may include alive, deleted,
+     * and expired documents.
+     */
+    public int getDocumentCount() {
+        return mNativeNumDocuments;
+    }
+
+    /** Returns number of schema types currently in the schema store. */
+    public int getSchemaTypeCount() {
+        return mNativeNumSchemaTypes;
+    }
+
+    /** Returns whether we had to reset the index, losing all data, as part of initialization. */
+    public boolean hasReset() {
+        return mHasReset;
+    }
+
+    /**
+     * Returns the status of the reset, if one was performed according to {@link #hasReset}.
+     *
+     * <p>If no value has been set, the default value is {@link AppSearchResult#RESULT_OK}.
+     */
+    @AppSearchResult.ResultCode
+    public int getResetStatusCode() {
+        return mResetStatusCode;
+    }
+
+    InitializeStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mHasDeSync = builder.mHasDeSync;
+        mPrepareSchemaAndNamespacesLatencyMillis = builder.mPrepareSchemaAndNamespacesLatencyMillis;
+        mPrepareVisibilityStoreLatencyMillis = builder.mPrepareVisibilityStoreLatencyMillis;
+        mNativeLatencyMillis = builder.mNativeLatencyMillis;
+        mNativeDocumentStoreRecoveryCause = builder.mNativeDocumentStoreRecoveryCause;
+        mNativeIndexRestorationCause = builder.mNativeIndexRestorationCause;
+        mNativeSchemaStoreRecoveryCause = builder.mNativeSchemaStoreRecoveryCause;
+        mNativeDocumentStoreRecoveryLatencyMillis =
+                builder.mNativeDocumentStoreRecoveryLatencyMillis;
+        mNativeIndexRestorationLatencyMillis = builder.mNativeIndexRestorationLatencyMillis;
+        mNativeSchemaStoreRecoveryLatencyMillis = builder.mNativeSchemaStoreRecoveryLatencyMillis;
+        mNativeDocumentStoreDataStatus = builder.mNativeDocumentStoreDataStatus;
+        mNativeNumDocuments = builder.mNativeNumDocuments;
+        mNativeNumSchemaTypes = builder.mNativeNumSchemaTypes;
+        mHasReset = builder.mHasReset;
+        mResetStatusCode = builder.mResetStatusCode;
+    }
+
+    /** Builder for {@link InitializeStats}. */
+    public static class Builder {
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+
+        int mTotalLatencyMillis;
+        boolean mHasDeSync;
+        int mPrepareSchemaAndNamespacesLatencyMillis;
+        int mPrepareVisibilityStoreLatencyMillis;
+        int mNativeLatencyMillis;
+        @RecoveryCause
+        int mNativeDocumentStoreRecoveryCause;
+        @RecoveryCause
+        int mNativeIndexRestorationCause;
+        @RecoveryCause
+        int mNativeSchemaStoreRecoveryCause;
+        int mNativeDocumentStoreRecoveryLatencyMillis;
+        int mNativeIndexRestorationLatencyMillis;
+        int mNativeSchemaStoreRecoveryLatencyMillis;
+        @DocumentStoreDataStatus
+        int mNativeDocumentStoreDataStatus;
+        int mNativeNumDocuments;
+        int mNativeNumSchemaTypes;
+        boolean mHasReset;
+        @AppSearchResult.ResultCode
+        int mResetStatusCode;
+
+        /** Sets the status of the initialization. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets the total latency of the initialization in milliseconds. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets whether the initialize() detects deSync.
+         *
+         * <p>If there is a deSync, it means AppSearch and IcingSearchEngine have an inconsistent
+         * view of what data should exist.
+         */
+        @NonNull
+        public Builder setHasDeSync(boolean hasDeSync) {
+            mHasDeSync = hasDeSync;
+            return this;
+        }
+
+        /** Sets time used to read and process the schema and namespaces. */
+        @NonNull
+        public Builder setPrepareSchemaAndNamespacesLatencyMillis(
+                int prepareSchemaAndNamespacesLatencyMillis) {
+            mPrepareSchemaAndNamespacesLatencyMillis = prepareSchemaAndNamespacesLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to read and process the visibility file. */
+        @NonNull
+        public Builder setPrepareVisibilityStoreLatencyMillis(
+                int prepareVisibilityStoreLatencyMillis) {
+            mPrepareVisibilityStoreLatencyMillis = prepareVisibilityStoreLatencyMillis;
+            return this;
+        }
+
+        /** Sets overall time used for the native function call. */
+        @NonNull
+        public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
+            mNativeLatencyMillis = nativeLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets recovery cause for document store.
+         *
+         * <p> Possible recovery causes for document store:
+         * <li> {@link InitializeStats#RECOVERY_CAUSE_DATA_LOSS}
+         * <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+         * <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+         */
+        @NonNull
+        public Builder setDocumentStoreRecoveryCause(
+                @RecoveryCause int documentStoreRecoveryCause) {
+            mNativeDocumentStoreRecoveryCause = documentStoreRecoveryCause;
+            return this;
+        }
+
+        /** Sets restoration cause for index store.
+         *
+         *  <p> Possible causes:
+         *      <li> {@link InitializeStats#DOCUMENT_STORE_DATA_STATUS_COMPLETE_LOSS}
+         *      <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+         *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+         */
+        @NonNull
+        public Builder setIndexRestorationCause(
+                @RecoveryCause int indexRestorationCause) {
+            mNativeIndexRestorationCause = indexRestorationCause;
+            return this;
+        }
+
+        /** Returns recovery cause for schema store.
+         *
+         *  <p> Possible causes:
+         *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+         */
+        @NonNull
+        public Builder setSchemaStoreRecoveryCause(
+                @RecoveryCause int schemaStoreRecoveryCause) {
+            mNativeSchemaStoreRecoveryCause = schemaStoreRecoveryCause;
+            return this;
+        }
+
+        /** Sets time used to recover the document store. */
+        @NonNull
+        public Builder setDocumentStoreRecoveryLatencyMillis(
+                int documentStoreRecoveryLatencyMillis) {
+            mNativeDocumentStoreRecoveryLatencyMillis = documentStoreRecoveryLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to restore the index. */
+        @NonNull
+        public Builder setIndexRestorationLatencyMillis(
+                int indexRestorationLatencyMillis) {
+            mNativeIndexRestorationLatencyMillis = indexRestorationLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to recover the schema store. */
+        @NonNull
+        public Builder setSchemaStoreRecoveryLatencyMillis(
+                int schemaStoreRecoveryLatencyMillis) {
+            mNativeSchemaStoreRecoveryLatencyMillis = schemaStoreRecoveryLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets Native Document Store Data status.
+         * status is defined in external/icing/proto/icing/proto/logging.proto
+         */
+        @NonNull
+        public Builder setDocumentStoreDataStatus(
+                @DocumentStoreDataStatus int documentStoreDataStatus) {
+            mNativeDocumentStoreDataStatus = documentStoreDataStatus;
+            return this;
+        }
+
+        /**
+         * Sets number of documents currently in document store. Those may include alive, deleted,
+         * and expired documents.
+         */
+        @NonNull
+        public Builder setDocumentCount(int numDocuments) {
+            mNativeNumDocuments = numDocuments;
+            return this;
+        }
+
+        /** Sets number of schema types currently in the schema store. */
+        @NonNull
+        public Builder setSchemaTypeCount(int numSchemaTypes) {
+            mNativeNumSchemaTypes = numSchemaTypes;
+            return this;
+        }
+
+        /** Sets whether we had to reset the index, losing all data, as part of initialization. */
+        @NonNull
+        public Builder setHasReset(boolean hasReset) {
+            mHasReset = hasReset;
+            return this;
+        }
+
+        /** Sets the status of the reset, if one was performed according to {@link #setHasReset}. */
+        @NonNull
+        public Builder setResetStatusCode(@AppSearchResult.ResultCode int resetStatusCode) {
+            mResetStatusCode = resetStatusCode;
+            return this;
+        }
+
+        /**
+         * Constructs a new {@link InitializeStats} from the contents of this
+         * {@link InitializeStats.Builder}
+         */
+        @NonNull
+        public InitializeStats build() {
+            return new InitializeStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/OptimizeStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/OptimizeStats.java
new file mode 100644
index 0000000..b7dcae0
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/OptimizeStats.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.core.util.Preconditions;
+
+/**
+ * Class holds detailed stats for Optimize.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class OptimizeStats {
+    /**
+     * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+     * internal state.
+     */
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+    private final int mNativeLatencyMillis;
+
+    // Time used to optimize the document store in millis.
+    private final int mNativeDocumentStoreOptimizeLatencyMillis;
+
+    // Time used to restore the index in millis.
+    private final int mNativeIndexRestorationLatencyMillis;
+
+    // Number of documents before the optimization.
+    private final int mNativeOriginalDocumentCount;
+
+    // Number of documents deleted during the optimization.
+    private final int mNativeDeletedDocumentCount;
+
+    // Number of documents expired during the optimization.
+    private final int mNativeExpiredDocumentCount;
+
+    // Size of storage in bytes before the optimization.
+    private final long mNativeStorageSizeBeforeBytes;
+
+    // Size of storage in bytes after the optimization.
+    private final long mNativeStorageSizeAfterBytes;
+
+    // The amount of time in millis since the last optimization ran calculated using wall clock time
+    private final long mNativeTimeSinceLastOptimizeMillis;
+
+    OptimizeStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mNativeLatencyMillis = builder.mNativeLatencyMillis;
+        mNativeDocumentStoreOptimizeLatencyMillis =
+                builder.mNativeDocumentStoreOptimizeLatencyMillis;
+        mNativeIndexRestorationLatencyMillis = builder.mNativeIndexRestorationLatencyMillis;
+        mNativeOriginalDocumentCount = builder.mNativeOriginalDocumentCount;
+        mNativeDeletedDocumentCount = builder.mNativeDeletedDocumentCount;
+        mNativeExpiredDocumentCount = builder.mNativeExpiredDocumentCount;
+        mNativeStorageSizeBeforeBytes = builder.mNativeStorageSizeBeforeBytes;
+        mNativeStorageSizeAfterBytes = builder.mNativeStorageSizeAfterBytes;
+        mNativeTimeSinceLastOptimizeMillis = builder.mNativeTimeSinceLastOptimizeMillis;
+    }
+
+    /** Returns status code for this optimization. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns total latency of this optimization in millis. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /** Returns how much time in millis spent in the native code. */
+    public int getNativeLatencyMillis() {
+        return mNativeLatencyMillis;
+    }
+
+    /** Returns time used to optimize the document store in millis. */
+    public int getDocumentStoreOptimizeLatencyMillis() {
+        return mNativeDocumentStoreOptimizeLatencyMillis;
+    }
+
+    /** Returns time used to restore the index in millis. */
+    public int getIndexRestorationLatencyMillis() {
+        return mNativeIndexRestorationLatencyMillis;
+    }
+
+    /** Returns number of documents before the optimization. */
+    public int getOriginalDocumentCount() {
+        return mNativeOriginalDocumentCount;
+    }
+
+    /** Returns number of documents deleted during the optimization. */
+    public int getDeletedDocumentCount() {
+        return mNativeDeletedDocumentCount;
+    }
+
+    /** Returns number of documents expired during the optimization. */
+    public int getExpiredDocumentCount() {
+        return mNativeExpiredDocumentCount;
+    }
+
+    /** Returns size of storage in bytes before the optimization. */
+    public long getStorageSizeBeforeBytes() {
+        return mNativeStorageSizeBeforeBytes;
+    }
+
+    /** Returns size of storage in bytes after the optimization. */
+    public long getStorageSizeAfterBytes() {
+        return mNativeStorageSizeAfterBytes;
+    }
+
+    /**
+     * Returns the amount of time in millis since the last optimization ran calculated using wall
+     * clock time.
+     */
+    public long getTimeSinceLastOptimizeMillis() {
+        return mNativeTimeSinceLastOptimizeMillis;
+    }
+
+    /** Builder for {@link RemoveStats}. */
+    public static class Builder {
+        /**
+         * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+         * internal state.
+         */
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+        int mTotalLatencyMillis;
+        int mNativeLatencyMillis;
+        int mNativeDocumentStoreOptimizeLatencyMillis;
+        int mNativeIndexRestorationLatencyMillis;
+        int mNativeOriginalDocumentCount;
+        int mNativeDeletedDocumentCount;
+        int mNativeExpiredDocumentCount;
+        long mNativeStorageSizeBeforeBytes;
+        long mNativeStorageSizeAfterBytes;
+        long mNativeTimeSinceLastOptimizeMillis;
+
+        /** Sets the status code. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets total latency in millis. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /** Sets native latency in millis. */
+        @NonNull
+        public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
+            mNativeLatencyMillis = nativeLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to optimize the document store. */
+        @NonNull
+        public Builder setDocumentStoreOptimizeLatencyMillis(
+                int documentStoreOptimizeLatencyMillis) {
+            mNativeDocumentStoreOptimizeLatencyMillis = documentStoreOptimizeLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to restore the index. */
+        @NonNull
+        public Builder setIndexRestorationLatencyMillis(int indexRestorationLatencyMillis) {
+            mNativeIndexRestorationLatencyMillis = indexRestorationLatencyMillis;
+            return this;
+        }
+
+        /** Sets number of documents before the optimization. */
+        @NonNull
+        public Builder setOriginalDocumentCount(int originalDocumentCount) {
+            mNativeOriginalDocumentCount = originalDocumentCount;
+            return this;
+        }
+
+        /** Sets number of documents deleted during the optimization. */
+        @NonNull
+        public Builder setDeletedDocumentCount(int deletedDocumentCount) {
+            mNativeDeletedDocumentCount = deletedDocumentCount;
+            return this;
+        }
+
+        /** Sets number of documents expired during the optimization. */
+        @NonNull
+        public Builder setExpiredDocumentCount(int expiredDocumentCount) {
+            mNativeExpiredDocumentCount = expiredDocumentCount;
+            return this;
+        }
+
+        /** Sets Storage size in bytes before optimization. */
+        @NonNull
+        public Builder setStorageSizeBeforeBytes(long storageSizeBeforeBytes) {
+            mNativeStorageSizeBeforeBytes = storageSizeBeforeBytes;
+            return this;
+        }
+
+        /** Sets storage size in bytes after optimization. */
+        @NonNull
+        public Builder setStorageSizeAfterBytes(long storageSizeAfterBytes) {
+            mNativeStorageSizeAfterBytes = storageSizeAfterBytes;
+            return this;
+        }
+
+        /**
+         * Sets the amount the time since the last optimize ran calculated using wall clock time.
+         */
+        @NonNull
+        public Builder setTimeSinceLastOptimizeMillis(long timeSinceLastOptimizeMillis) {
+            mNativeTimeSinceLastOptimizeMillis = timeSinceLastOptimizeMillis;
+            return this;
+        }
+
+        /** Creates a {@link OptimizeStats}. */
+        @NonNull
+        public OptimizeStats build() {
+            return new OptimizeStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
new file mode 100644
index 0000000..3bcc5cb
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.core.util.Preconditions;
+
+/**
+ * A class for holding detailed stats to log for each individual document put by a
+ * {@link androidx.appsearch.app.AppSearchSession#put} call.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class PutDocumentStats {
+    @NonNull
+    private final String mPackageName;
+    @NonNull
+    private final String mDatabase;
+    /**
+     * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+     * internal state.
+     */
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+
+    /** Time used to generate a document proto from a Bundle. */
+    private final int mGenerateDocumentProtoLatencyMillis;
+
+    /** Time used to rewrite types and namespaces in the document. */
+    private final int mRewriteDocumentTypesLatencyMillis;
+
+    /** Overall time used for the native function call. */
+    private final int mNativeLatencyMillis;
+
+    /** Time used to store the document. */
+    private final int mNativeDocumentStoreLatencyMillis;
+
+    /** Time used to index the document. It doesn't include the time to merge indices. */
+    private final int mNativeIndexLatencyMillis;
+
+    /** Time used to merge the indices. */
+    private final int mNativeIndexMergeLatencyMillis;
+
+    /** Document size in bytes. */
+    private final int mNativeDocumentSizeBytes;
+
+    /** Number of tokens added to the index. */
+    private final int mNativeNumTokensIndexed;
+
+    /**
+     * Whether the number of tokens to be indexed exceeded the max number of tokens per
+     * document.
+     */
+    private final boolean mNativeExceededMaxNumTokens;
+
+    PutDocumentStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mGenerateDocumentProtoLatencyMillis = builder.mGenerateDocumentProtoLatencyMillis;
+        mRewriteDocumentTypesLatencyMillis = builder.mRewriteDocumentTypesLatencyMillis;
+        mNativeLatencyMillis = builder.mNativeLatencyMillis;
+        mNativeDocumentStoreLatencyMillis = builder.mNativeDocumentStoreLatencyMillis;
+        mNativeIndexLatencyMillis = builder.mNativeIndexLatencyMillis;
+        mNativeIndexMergeLatencyMillis = builder.mNativeIndexMergeLatencyMillis;
+        mNativeDocumentSizeBytes = builder.mNativeDocumentSizeBytes;
+        mNativeNumTokensIndexed = builder.mNativeNumTokensIndexed;
+        mNativeExceededMaxNumTokens = builder.mNativeExceededMaxNumTokens;
+    }
+
+    /** Returns calling package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns calling database name. */
+    @NonNull
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns status code for this putDocument. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns total latency of this putDocument in millis. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /** Returns time spent on generating document proto, in milliseconds. */
+    public int getGenerateDocumentProtoLatencyMillis() {
+        return mGenerateDocumentProtoLatencyMillis;
+    }
+
+    /** Returns time spent on rewriting types and namespaces in document, in milliseconds. */
+    public int getRewriteDocumentTypesLatencyMillis() {
+        return mRewriteDocumentTypesLatencyMillis;
+    }
+
+    /** Returns time spent in native, in milliseconds. */
+    public int getNativeLatencyMillis() {
+        return mNativeLatencyMillis;
+    }
+
+    /** Returns time spent on document store, in milliseconds. */
+    public int getNativeDocumentStoreLatencyMillis() {
+        return mNativeDocumentStoreLatencyMillis;
+    }
+
+    /** Returns time spent on indexing, in milliseconds. */
+    public int getNativeIndexLatencyMillis() {
+        return mNativeIndexLatencyMillis;
+    }
+
+    /** Returns time spent on merging indices, in milliseconds. */
+    public int getNativeIndexMergeLatencyMillis() {
+        return mNativeIndexMergeLatencyMillis;
+    }
+
+    /** Returns document size, in bytes. */
+    public int getNativeDocumentSizeBytes() {
+        return mNativeDocumentSizeBytes;
+    }
+
+    /** Returns number of tokens indexed. */
+    public int getNativeNumTokensIndexed() {
+        return mNativeNumTokensIndexed;
+    }
+
+    /**
+     * Returns whether the number of tokens to be indexed exceeded the max number of tokens per
+     * document.
+     */
+    public boolean getNativeExceededMaxNumTokens() {
+        return mNativeExceededMaxNumTokens;
+    }
+
+    /** Builder for {@link PutDocumentStats}. */
+    public static class Builder {
+        @NonNull
+        final String mPackageName;
+        @NonNull
+        final String mDatabase;
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+        int mTotalLatencyMillis;
+        int mGenerateDocumentProtoLatencyMillis;
+        int mRewriteDocumentTypesLatencyMillis;
+        int mNativeLatencyMillis;
+        int mNativeDocumentStoreLatencyMillis;
+        int mNativeIndexLatencyMillis;
+        int mNativeIndexMergeLatencyMillis;
+        int mNativeDocumentSizeBytes;
+        int mNativeNumTokensIndexed;
+        boolean mNativeExceededMaxNumTokens;
+
+        /** Builder for {@link PutDocumentStats} */
+        public Builder(@NonNull String packageName, @NonNull String database) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            mDatabase = Preconditions.checkNotNull(database);
+        }
+
+        /** Sets the status code. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets total latency in millis. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /** Sets how much time we spend for generating document proto, in milliseconds. */
+        @NonNull
+        public Builder setGenerateDocumentProtoLatencyMillis(
+                int generateDocumentProtoLatencyMillis) {
+            mGenerateDocumentProtoLatencyMillis = generateDocumentProtoLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets how much time we spend for rewriting types and namespaces in document, in
+         * milliseconds.
+         */
+        @NonNull
+        public Builder setRewriteDocumentTypesLatencyMillis(int rewriteDocumentTypesLatencyMillis) {
+            mRewriteDocumentTypesLatencyMillis = rewriteDocumentTypesLatencyMillis;
+            return this;
+        }
+
+        /** Sets the native latency, in milliseconds. */
+        @NonNull
+        public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
+            mNativeLatencyMillis = nativeLatencyMillis;
+            return this;
+        }
+
+        /** Sets how much time we spend on document store, in milliseconds. */
+        @NonNull
+        public Builder setNativeDocumentStoreLatencyMillis(int nativeDocumentStoreLatencyMillis) {
+            mNativeDocumentStoreLatencyMillis = nativeDocumentStoreLatencyMillis;
+            return this;
+        }
+
+        /** Sets the native index latency, in milliseconds. */
+        @NonNull
+        public Builder setNativeIndexLatencyMillis(int nativeIndexLatencyMillis) {
+            mNativeIndexLatencyMillis = nativeIndexLatencyMillis;
+            return this;
+        }
+
+        /** Sets how much time we spend on merging indices, in milliseconds. */
+        @NonNull
+        public Builder setNativeIndexMergeLatencyMillis(int nativeIndexMergeLatencyMillis) {
+            mNativeIndexMergeLatencyMillis = nativeIndexMergeLatencyMillis;
+            return this;
+        }
+
+        /** Sets document size, in bytes. */
+        @NonNull
+        public Builder setNativeDocumentSizeBytes(int nativeDocumentSizeBytes) {
+            mNativeDocumentSizeBytes = nativeDocumentSizeBytes;
+            return this;
+        }
+
+        /** Sets number of tokens indexed in native. */
+        @NonNull
+        public Builder setNativeNumTokensIndexed(int nativeNumTokensIndexed) {
+            mNativeNumTokensIndexed = nativeNumTokensIndexed;
+            return this;
+        }
+
+        /**
+         * Sets whether the number of tokens to be indexed exceeded the max number of tokens per
+         * document.
+         */
+        @NonNull
+        public Builder setNativeExceededMaxNumTokens(boolean nativeExceededMaxNumTokens) {
+            mNativeExceededMaxNumTokens = nativeExceededMaxNumTokens;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link PutDocumentStats} object from the contents of this
+         * {@link Builder} instance.
+         */
+        @NonNull
+        public PutDocumentStats build() {
+            return new PutDocumentStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
new file mode 100644
index 0000000..3a63aa0
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.SearchSpec;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class holds detailed stats for
+ * {@link androidx.appsearch.app.AppSearchSession#remove(RemoveByDocumentIdRequest)} and
+ * {@link androidx.appsearch.app.AppSearchSession#remove(String, SearchSpec)}
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class RemoveStats {
+    @IntDef(value = {
+            // It needs to be sync with DeleteType.Code in
+            // external/icing/proto/icing/proto/logging.proto#DeleteStatsProto
+            UNKNOWN,
+            SINGLE,
+            QUERY,
+            NAMESPACE,
+            SCHEMA_TYPE,
+
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeleteType {
+    }
+
+    /** Default. Should never be used. */
+    public static final int UNKNOWN = 0;
+    /** Delete by namespace + id. */
+    public static final int SINGLE = 1;
+    /** Delete by query. */
+    public static final int QUERY = 2;
+    /** Delete by namespace. */
+    public static final int NAMESPACE = 3;
+    /** Delete by schema type. */
+    public static final int SCHEMA_TYPE = 4;
+
+    @NonNull
+    private final String mPackageName;
+    @NonNull
+    private final String mDatabase;
+    /**
+     * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+     * internal state.
+     */
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+    private final int mNativeLatencyMillis;
+    @DeleteType
+    private final int mNativeDeleteType;
+    private final int mNativeNumDocumentsDeleted;
+
+    RemoveStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mNativeLatencyMillis = builder.mNativeLatencyMillis;
+        mNativeDeleteType = builder.mNativeDeleteType;
+        mNativeNumDocumentsDeleted = builder.mNativeNumDocumentsDeleted;
+    }
+
+    /** Returns calling package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns calling database name. */
+    @NonNull
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns status code for this remove. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns total latency of this remove in millis. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /** Returns how much time in millis spent in the native code. */
+    public int getNativeLatencyMillis() {
+        return mNativeLatencyMillis;
+    }
+
+    /** Returns what type of delete for this remove call. */
+    @DeleteType
+    public int getDeleteType() {
+        return mNativeDeleteType;
+    }
+
+    /** Returns how many documents get deleted in this call. */
+    public int getDeletedDocumentCount() {
+        return mNativeNumDocumentsDeleted;
+    }
+
+    /** Builder for {@link RemoveStats}. */
+    public static class Builder {
+        @NonNull
+        final String mPackageName;
+        @NonNull
+        final String mDatabase;
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+        int mTotalLatencyMillis;
+        int mNativeLatencyMillis;
+        @DeleteType
+        int mNativeDeleteType;
+        int mNativeNumDocumentsDeleted;
+
+        /** Constructor for the {@link Builder}. */
+        public Builder(@NonNull String packageName, @NonNull String database) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            mDatabase = Preconditions.checkNotNull(database);
+        }
+
+        /** Sets the status code. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets total latency in millis. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /** Sets native latency in millis. */
+        @NonNull
+        public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
+            mNativeLatencyMillis = nativeLatencyMillis;
+            return this;
+        }
+
+        /** Sets delete type for this call. */
+        @NonNull
+        public Builder setDeleteType(@DeleteType int nativeDeleteType) {
+            mNativeDeleteType = nativeDeleteType;
+            return this;
+        }
+
+        /** Sets how many documents get deleted for this call. */
+        @NonNull
+        public Builder setDeletedDocumentCount(int nativeNumDocumentsDeleted) {
+            mNativeNumDocumentsDeleted = nativeNumDocumentsDeleted;
+            return this;
+        }
+
+        /** Creates a {@link RemoveStats}. */
+        @NonNull
+        public RemoveStats build() {
+            return new RemoveStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SchemaMigrationStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SchemaMigrationStats.java
new file mode 100644
index 0000000..dc3f6f4
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SchemaMigrationStats.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.core.util.Preconditions;
+
+/**
+ * Class holds detailed stats for Schema migration.
+ *
+ * @hide
+ */
+// TODO(b/173532925): Hides getter and setter functions for accessing {@code
+//  mFirstSetSchemaLatencyMillis} and {@code mSecondSetSchemaLatencyMillis} field.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SchemaMigrationStats {
+    /** GetSchema latency in milliseconds. */
+    private final int mGetSchemaLatencyMillis;
+
+    /**
+     * Latency of querying all documents that need to be migrated to new version and transforming
+     * documents to new version in milliseconds.
+     */
+    private final int mQueryAndTransformLatencyMillis;
+
+    private final int mFirstSetSchemaLatencyMillis;
+
+    private final int mSecondSetSchemaLatencyMillis;
+
+    /** Latency of putting migrated document to Icing lib in milliseconds. */
+    private final int mSaveDocumentLatencyMillis;
+
+    private final int mMigratedDocumentCount;
+
+    private final int mSavedDocumentCount;
+
+    SchemaMigrationStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mGetSchemaLatencyMillis = builder.mGetSchemaLatencyMillis;
+        mQueryAndTransformLatencyMillis = builder.mQueryAndTransformLatencyMillis;
+        mFirstSetSchemaLatencyMillis = builder.mFirstSetSchemaLatencyMillis;
+        mSecondSetSchemaLatencyMillis = builder.mSecondSetSchemaLatencyMillis;
+        mSaveDocumentLatencyMillis = builder.mSaveDocumentLatencyMillis;
+        mMigratedDocumentCount = builder.mMigratedDocumentCount;
+        mSavedDocumentCount = builder.mSavedDocumentCount;
+    }
+
+    /** Returns GetSchema latency in milliseconds. */
+    public int getGetSchemaLatencyMillis() {
+        return mGetSchemaLatencyMillis;
+    }
+
+    /**
+     * Returns latency of querying all documents that need to be migrated to new version and
+     * transforming documents to new version in milliseconds.
+     */
+    public int getQueryAndTransformLatencyMillis() {
+        return mQueryAndTransformLatencyMillis;
+    }
+
+    /**
+     * Returns latency of first SetSchema action in milliseconds.
+     *
+     * <p>If all schema fields are backward compatible, the schema will be successful set to Icing.
+     * Otherwise, we will retrieve incompatible types here.
+     *
+     * <p>Please see {@link SetSchemaRequest} for what is "incompatible".
+     */
+    public int getFirstSetSchemaLatencyMillis() {
+        return mFirstSetSchemaLatencyMillis;
+    }
+
+    /**
+     * Returns latency of second SetSchema action in milliseconds.
+     *
+     * <p>If all schema fields are backward compatible, the schema will be successful set to
+     * Icing in the first setSchema action and this value will be 0. Otherwise, schema types will
+     * be set to Icing by this action.
+     */
+    public int getSecondSetSchemaLatencyMillis() {
+        return mSecondSetSchemaLatencyMillis;
+    }
+
+    /** Returns latency of putting migrated document to Icing lib in milliseconds. */
+    public int getSaveDocumentLatencyMillis() {
+        return mSaveDocumentLatencyMillis;
+    }
+
+    /** Returns number of migrated documents. */
+    public int getMigratedDocumentCount() {
+        return mMigratedDocumentCount;
+    }
+
+    /** Returns number of updated documents which are saved in Icing lib. */
+    public int getSavedDocumentCount() {
+        return mSavedDocumentCount;
+    }
+
+    /** Builder for {@link SchemaMigrationStats}. */
+    public static class Builder {
+        int mGetSchemaLatencyMillis;
+        int mQueryAndTransformLatencyMillis;
+        int mFirstSetSchemaLatencyMillis;
+        int mSecondSetSchemaLatencyMillis;
+        int mSaveDocumentLatencyMillis;
+        int mMigratedDocumentCount;
+        int mSavedDocumentCount;
+
+        /** Sets latency for the GetSchema action in milliseconds. */
+        @NonNull
+        public SchemaMigrationStats.Builder setGetSchemaLatencyMillis(int getSchemaLatencyMillis) {
+            mGetSchemaLatencyMillis = getSchemaLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets latency for querying all documents that need to be migrated to new version and
+         * transforming documents to new version in milliseconds.
+         */
+        @NonNull
+        public SchemaMigrationStats.Builder setQueryAndTransformLatencyMillis(
+                int queryAndTransformLatencyMillis) {
+            mQueryAndTransformLatencyMillis = queryAndTransformLatencyMillis;
+            return this;
+        }
+
+        /** Sets latency of first SetSchema action in milliseconds. */
+        @NonNull
+        public SchemaMigrationStats.Builder setFirstSetSchemaLatencyMillis(
+                int firstSetSchemaLatencyMillis) {
+            mFirstSetSchemaLatencyMillis = firstSetSchemaLatencyMillis;
+            return this;
+        }
+
+        /** Sets latency of second SetSchema action in milliseconds. */
+        @NonNull
+        public SchemaMigrationStats.Builder setSecondSetSchemaLatencyMillis(
+                int secondSetSchemaLatencyMillis) {
+            mSecondSetSchemaLatencyMillis = secondSetSchemaLatencyMillis;
+            return this;
+        }
+
+        /** Sets latency for putting migrated document to Icing lib in milliseconds. */
+        @NonNull
+        public SchemaMigrationStats.Builder setSaveDocumentLatencyMillis(
+                int saveDocumentLatencyMillis) {
+            mSaveDocumentLatencyMillis = saveDocumentLatencyMillis;
+            return this;
+        }
+
+        /** Sets number of migrated documents. */
+        @NonNull
+        public SchemaMigrationStats.Builder setMigratedDocumentCount(int migratedDocumentCount) {
+            mMigratedDocumentCount = migratedDocumentCount;
+            return this;
+        }
+
+        /** Sets number of updated documents which are saved in Icing lib. */
+        @NonNull
+        public SchemaMigrationStats.Builder setSavedDocumentCount(int savedDocumentCount) {
+            mSavedDocumentCount = savedDocumentCount;
+            return this;
+        }
+
+        /**
+         * Builds a new {@link SchemaMigrationStats} from the {@link SchemaMigrationStats.Builder}.
+         */
+        @NonNull
+        public SchemaMigrationStats build() {
+            return new SchemaMigrationStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
new file mode 100644
index 0000000..193168c
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.SearchSpec;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class holds detailed stats for
+ * {@link androidx.appsearch.app.AppSearchSession#search(String, SearchSpec)}
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchStats {
+    @IntDef(value = {
+            // Searches apps' own documents.
+            VISIBILITY_SCOPE_LOCAL,
+            // Searches the global documents. Including platform surfaceable and 3p-access.
+            VISIBILITY_SCOPE_GLOBAL,
+            // TODO(b/173532925) Add THIRD_PARTY_ACCESS once we can distinguish platform
+            //  surfaceable from 3p access(right both of them are categorized as
+            //  VISIBILITY_SCOPE_GLOBAL)
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VisibilityScope {
+    }
+
+    // Searches apps' own documents.
+    public static final int VISIBILITY_SCOPE_LOCAL = 1;
+    // Searches the global documents. Including platform surfaceable and 3p-access.
+    public static final int VISIBILITY_SCOPE_GLOBAL = 2;
+
+    // TODO(b/173532925): Add a field searchType to indicate where the search is used(normal
+    //  query vs in removeByQuery vs during migration)
+
+    @NonNull
+    private final String mPackageName;
+    @Nullable
+    private final String mDatabase;
+    /**
+     * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+     * internal state.
+     */
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+    /** Time used to rewrite the search spec. */
+    private final int mRewriteSearchSpecLatencyMillis;
+    /** Time used to rewrite the search results. */
+    private final int mRewriteSearchResultLatencyMillis;
+    /** Defines the scope the query is searching over */
+    @VisibilityScope
+    private final int mVisibilityScope;
+    /** Overall time used for the native function call. */
+    private final int mNativeLatencyMillis;
+    /** Number of terms in the query string. */
+    private final int mNativeNumTerms;
+    /** Length of the query string. */
+    private final int mNativeQueryLength;
+    /** Number of namespaces filtered. */
+    private final int mNativeNumNamespacesFiltered;
+    /** Number of schema types filtered. */
+    private final int mNativeNumSchemaTypesFiltered;
+    /** The requested number of results in one page. */
+    private final int mNativeRequestedPageSize;
+    /** The actual number of results returned in the current page. */
+    private final int mNativeNumResultsReturnedCurrentPage;
+    /**
+     * Whether the function call is querying the first page. If it's
+     * not, Icing will fetch the results from cache so that some steps
+     * may be skipped.
+     */
+    private final boolean mNativeIsFirstPage;
+    /**
+     * Time used to parse the query, including 2 parts: tokenizing and
+     * transforming tokens into an iterator tree.
+     */
+    private final int mNativeParseQueryLatencyMillis;
+    /** Strategy of scoring and ranking. */
+    @SearchSpec.RankingStrategy
+    private final int mNativeRankingStrategy;
+    /** Number of documents scored. */
+    private final int mNativeNumDocumentsScored;
+    /** Time used to score the raw results. */
+    private final int mNativeScoringLatencyMillis;
+    /** Time used to rank the scored results. */
+    private final int mNativeRankingLatencyMillis;
+    /**
+     * Time used to fetch the document protos. Note that it includes the
+     * time to snippet if {@link SearchStats#mNativeNumResultsWithSnippets} is greater than 0.
+     */
+    private final int mNativeDocumentRetrievingLatencyMillis;
+    /** How many snippets are calculated. */
+    private final int mNativeNumResultsWithSnippets;
+
+    SearchStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mRewriteSearchSpecLatencyMillis = builder.mRewriteSearchSpecLatencyMillis;
+        mRewriteSearchResultLatencyMillis = builder.mRewriteSearchResultLatencyMillis;
+        mVisibilityScope = builder.mVisibilityScope;
+        mNativeLatencyMillis = builder.mNativeLatencyMillis;
+        mNativeNumTerms = builder.mNativeNumTerms;
+        mNativeQueryLength = builder.mNativeQueryLength;
+        mNativeNumNamespacesFiltered = builder.mNativeNumNamespacesFiltered;
+        mNativeNumSchemaTypesFiltered = builder.mNativeNumSchemaTypesFiltered;
+        mNativeRequestedPageSize = builder.mNativeRequestedPageSize;
+        mNativeNumResultsReturnedCurrentPage = builder.mNativeNumResultsReturnedCurrentPage;
+        mNativeIsFirstPage = builder.mNativeIsFirstPage;
+        mNativeParseQueryLatencyMillis = builder.mNativeParseQueryLatencyMillis;
+        mNativeRankingStrategy = builder.mNativeRankingStrategy;
+        mNativeNumDocumentsScored = builder.mNativeNumDocumentsScored;
+        mNativeScoringLatencyMillis = builder.mNativeScoringLatencyMillis;
+        mNativeRankingLatencyMillis = builder.mNativeRankingLatencyMillis;
+        mNativeNumResultsWithSnippets = builder.mNativeNumResultsWithSnippets;
+        mNativeDocumentRetrievingLatencyMillis = builder.mNativeDocumentRetrievingLatencyMillis;
+    }
+
+    /** Returns the package name of the session. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Returns the database name of the session.
+     *
+     * @return database name used by the session. {@code null} if and only if it is a
+     * global search(visibilityScope is {@link SearchStats#VISIBILITY_SCOPE_GLOBAL}).
+     */
+    @Nullable
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns status of the search. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns the total latency of the search. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /** Returns how much time spent on rewriting the {@link SearchSpec}. */
+    public int getRewriteSearchSpecLatencyMillis() {
+        return mRewriteSearchSpecLatencyMillis;
+    }
+
+    /** Returns how much time spent on rewriting the {@link androidx.appsearch.app.SearchResult}. */
+    public int getRewriteSearchResultLatencyMillis() {
+        return mRewriteSearchResultLatencyMillis;
+    }
+
+    /** Returns the visibility scope of the search. */
+    @VisibilityScope
+    public int getVisibilityScope() {
+        return mVisibilityScope;
+    }
+
+    /** Returns how much time spent on the native calls. */
+    public int getNativeLatencyMillis() {
+        return mNativeLatencyMillis;
+    }
+
+    /** Returns number of terms in the search string. */
+    public int getTermCount() {
+        return mNativeNumTerms;
+    }
+
+    /** Returns the length of the search string. */
+    public int getQueryLength() {
+        return mNativeQueryLength;
+    }
+
+    /** Returns number of namespaces filtered. */
+    public int getFilteredNamespaceCount() {
+        return mNativeNumNamespacesFiltered;
+    }
+
+    /** Returns number of schema types filtered. */
+    public int getFilteredSchemaTypeCount() {
+        return mNativeNumSchemaTypesFiltered;
+    }
+
+    /** Returns the requested number of results in one page. */
+    public int getRequestedPageSize() {
+        return mNativeRequestedPageSize;
+    }
+
+    /** Returns the actual number of results returned in the current page. */
+    public int getCurrentPageReturnedResultCount() {
+        return mNativeNumResultsReturnedCurrentPage;
+    }
+
+    // TODO(b/185184738) Make it an integer to show how many pages having been returned.
+    /** Returns whether the function call is querying the first page. */
+    public boolean isFirstPage() {
+        return mNativeIsFirstPage;
+    }
+
+    /**
+     * Returns time used to parse the query, including 2 parts: tokenizing and transforming
+     * tokens into an iterator tree.
+     */
+    public int getParseQueryLatencyMillis() {
+        return mNativeParseQueryLatencyMillis;
+    }
+
+    /** Returns strategy of scoring and ranking. */
+    @SearchSpec.RankingStrategy
+    public int getRankingStrategy() {
+        return mNativeRankingStrategy;
+    }
+
+    /** Returns number of documents scored. */
+    public int getScoredDocumentCount() {
+        return mNativeNumDocumentsScored;
+    }
+
+    /** Returns time used to score the raw results. */
+    public int getScoringLatencyMillis() {
+        return mNativeScoringLatencyMillis;
+    }
+
+    /** Returns time used to rank the scored results. */
+    public int getRankingLatencyMillis() {
+        return mNativeRankingLatencyMillis;
+    }
+
+    /**
+     * Returns time used to fetch the document protos. Note that it includes the
+     * time to snippet if {@link SearchStats#mNativeNumResultsWithSnippets} is not zero.
+     */
+    public int getDocumentRetrievingLatencyMillis() {
+        return mNativeDocumentRetrievingLatencyMillis;
+    }
+
+    /** Returns the number of the results in the page returned were snippeted. */
+    public int getResultWithSnippetsCount() {
+        return mNativeNumResultsWithSnippets;
+    }
+
+    /** Builder for {@link SearchStats} */
+    public static class Builder {
+        @NonNull
+        final String mPackageName;
+        @Nullable
+        String mDatabase;
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+        int mTotalLatencyMillis;
+        int mRewriteSearchSpecLatencyMillis;
+        int mRewriteSearchResultLatencyMillis;
+        int mVisibilityScope;
+        int mNativeLatencyMillis;
+        int mNativeNumTerms;
+        int mNativeQueryLength;
+        int mNativeNumNamespacesFiltered;
+        int mNativeNumSchemaTypesFiltered;
+        int mNativeRequestedPageSize;
+        int mNativeNumResultsReturnedCurrentPage;
+        boolean mNativeIsFirstPage;
+        int mNativeParseQueryLatencyMillis;
+        int mNativeRankingStrategy;
+        int mNativeNumDocumentsScored;
+        int mNativeScoringLatencyMillis;
+        int mNativeRankingLatencyMillis;
+        int mNativeNumResultsWithSnippets;
+        int mNativeDocumentRetrievingLatencyMillis;
+
+        /**
+         * Constructor
+         *
+         * @param visibilityScope scope for the corresponding search.
+         * @param packageName     name of the calling package.
+         */
+        public Builder(@VisibilityScope int visibilityScope, @NonNull String packageName) {
+            mVisibilityScope = visibilityScope;
+            mPackageName = Preconditions.checkNotNull(packageName);
+        }
+
+        /** Sets the database used by the session. */
+        @NonNull
+        public Builder setDatabase(@NonNull String database) {
+            mDatabase = Preconditions.checkNotNull(database);
+            return this;
+        }
+
+        /** Sets the status of the search. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets total latency for the search. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to rewrite the search spec. */
+        @NonNull
+        public Builder setRewriteSearchSpecLatencyMillis(int rewriteSearchSpecLatencyMillis) {
+            mRewriteSearchSpecLatencyMillis = rewriteSearchSpecLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to rewrite the search results. */
+        @NonNull
+        public Builder setRewriteSearchResultLatencyMillis(int rewriteSearchResultLatencyMillis) {
+            mRewriteSearchResultLatencyMillis = rewriteSearchResultLatencyMillis;
+            return this;
+        }
+
+        /** Sets overall time used for the native function calls. */
+        @NonNull
+        public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
+            mNativeLatencyMillis = nativeLatencyMillis;
+            return this;
+        }
+
+        /** Sets number of terms in the search string. */
+        @NonNull
+        public Builder setTermCount(int termCount) {
+            mNativeNumTerms = termCount;
+            return this;
+        }
+
+        /** Sets length of the search string. */
+        @NonNull
+        public Builder setQueryLength(int queryLength) {
+            mNativeQueryLength = queryLength;
+            return this;
+        }
+
+        /** Sets number of namespaces filtered. */
+        @NonNull
+        public Builder setFilteredNamespaceCount(int filteredNamespaceCount) {
+            mNativeNumNamespacesFiltered = filteredNamespaceCount;
+            return this;
+        }
+
+        /** Sets number of schema types filtered. */
+        @NonNull
+        public Builder setFilteredSchemaTypeCount(int filteredSchemaTypeCount) {
+            mNativeNumSchemaTypesFiltered = filteredSchemaTypeCount;
+            return this;
+        }
+
+        /** Sets the requested number of results in one page. */
+        @NonNull
+        public Builder setRequestedPageSize(int requestedPageSize) {
+            mNativeRequestedPageSize = requestedPageSize;
+            return this;
+        }
+
+        /** Sets the actual number of results returned in the current page. */
+        @NonNull
+        public Builder setCurrentPageReturnedResultCount(
+                int currentPageReturnedResultCount) {
+            mNativeNumResultsReturnedCurrentPage = currentPageReturnedResultCount;
+            return this;
+        }
+
+        /**
+         * Sets whether the function call is querying the first page. If it's
+         * not, Icing will fetch the results from cache so that some steps
+         * may be skipped.
+         */
+        @NonNull
+        public Builder setIsFirstPage(boolean nativeIsFirstPage) {
+            mNativeIsFirstPage = nativeIsFirstPage;
+            return this;
+        }
+
+        /**
+         * Sets time used to parse the query, including 2 parts: tokenizing and
+         * transforming tokens into an iterator tree.
+         */
+        @NonNull
+        public Builder setParseQueryLatencyMillis(int parseQueryLatencyMillis) {
+            mNativeParseQueryLatencyMillis = parseQueryLatencyMillis;
+            return this;
+        }
+
+        /** Sets strategy of scoring and ranking. */
+        @NonNull
+        public Builder setRankingStrategy(
+                @SearchSpec.RankingStrategy int rankingStrategy) {
+            mNativeRankingStrategy = rankingStrategy;
+            return this;
+        }
+
+        /** Sets number of documents scored. */
+        @NonNull
+        public Builder setScoredDocumentCount(int scoredDocumentCount) {
+            mNativeNumDocumentsScored = scoredDocumentCount;
+            return this;
+        }
+
+        /** Sets time used to score the raw results. */
+        @NonNull
+        public Builder setScoringLatencyMillis(int scoringLatencyMillis) {
+            mNativeScoringLatencyMillis = scoringLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to rank the scored results. */
+        @NonNull
+        public Builder setRankingLatencyMillis(int rankingLatencyMillis) {
+            mNativeRankingLatencyMillis = rankingLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to fetch the document protos. */
+        @NonNull
+        public Builder setDocumentRetrievingLatencyMillis(
+                int documentRetrievingLatencyMillis) {
+            mNativeDocumentRetrievingLatencyMillis = documentRetrievingLatencyMillis;
+            return this;
+        }
+
+        /** Sets how many snippets are calculated. */
+        @NonNull
+        public Builder setResultWithSnippetsCount(int resultWithSnippetsCount) {
+            mNativeNumResultsWithSnippets = resultWithSnippetsCount;
+            return this;
+        }
+
+        /**
+         * Constructs a new {@link SearchStats} from the contents of this
+         * {@link SearchStats.Builder}.
+         */
+        @NonNull
+        public SearchStats build() {
+            if (mDatabase == null) {
+                Preconditions.checkState(mVisibilityScope != SearchStats.VISIBILITY_SCOPE_LOCAL,
+                        "database can not be null if visibilityScope is local.");
+            }
+
+            return new SearchStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SetSchemaStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SetSchemaStats.java
new file mode 100644
index 0000000..314f964
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/SetSchemaStats.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.core.util.Preconditions;
+
+/**
+ * Class holds detailed stats for
+ * {@link androidx.appsearch.app.AppSearchSession#setSchema(SetSchemaRequest)}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SetSchemaStats {
+    @NonNull
+    private final String mPackageName;
+
+    @NonNull
+    private final String mDatabase;
+
+    /**
+     * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+     * internal state.
+     */
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+
+    /**
+     * Stores stats of SchemaMigration in SetSchema process. Is {@code null} if no schema migration
+     * is needed.
+     */
+    @Nullable
+    private final SchemaMigrationStats mSchemaMigrationStats;
+
+    private final int mTotalLatencyMillis;
+
+    /** Number of newly added schema types. */
+    private final int mNewTypeCount;
+
+    /** Number of deleted schema types. */
+    private final int mDeletedTypeCount;
+
+    /** Number of compatible schema type changes. */
+    private final int mCompatibleTypeChangeCount;
+
+    /** Number of index-incompatible schema type changes. */
+    private final int mIndexIncompatibleTypeChangeCount;
+
+    /** Number of backwards-incompatible schema type changes. */
+    private final int mBackwardsIncompatibleTypeChangeCount;
+
+    SetSchemaStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mStatusCode = builder.mStatusCode;
+        mSchemaMigrationStats = builder.mSchemaMigrationStats;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mNewTypeCount = builder.mNewTypeCount;
+        mDeletedTypeCount = builder.mDeletedTypeCount;
+        mCompatibleTypeChangeCount = builder.mCompatibleTypeChangeCount;
+        mIndexIncompatibleTypeChangeCount = builder.mIndexIncompatibleTypeChangeCount;
+        mBackwardsIncompatibleTypeChangeCount = builder.mBackwardsIncompatibleTypeChangeCount;
+    }
+
+    /** Returns calling package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns calling database name. */
+    @NonNull
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns status of the SetSchema action. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /**
+     * Returns the status of schema migration, if migration is executed during the SetSchema
+     * process. Otherwise, returns {@code null}.
+     */
+    @Nullable
+    public SchemaMigrationStats getSchemaMigrationStats() {
+        return mSchemaMigrationStats;
+    }
+
+    /** Returns the total latency of the SetSchema action. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /** Returns number of newly added schema types. */
+    public int getNewTypeCount() {
+        return mNewTypeCount;
+    }
+
+    /** Returns number of deleted schema types. */
+    public int getDeletedTypeCount() {
+        return mDeletedTypeCount;
+    }
+
+    /** Returns number of compatible type changes. */
+    public int getCompatibleTypeChangeCount() {
+        return mCompatibleTypeChangeCount;
+    }
+
+    /**
+     * Returns number of index-incompatible type change.
+     *
+     * <p>An index-incompatible type change is one that affects how pre-existing data should be
+     * searched over, such as modifying the {@code IndexingType} of an existing property.
+     */
+    public int getIndexIncompatibleTypeChangeCount() {
+        return mIndexIncompatibleTypeChangeCount;
+    }
+
+    /**
+     * Returns number of backwards-incompatible type change.
+     *
+     * <p>For details on what constitutes a backward-incompatible type change, please see
+     * {@link androidx.appsearch.app.SetSchemaRequest}.
+     */
+    public int getBackwardsIncompatibleTypeChangeCount() {
+        return mBackwardsIncompatibleTypeChangeCount;
+    }
+
+    /** Builder for {@link SetSchemaStats}. */
+    public static class Builder {
+        @NonNull
+        final String mPackageName;
+        @NonNull
+        final String mDatabase;
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+        @Nullable
+        SchemaMigrationStats mSchemaMigrationStats;
+        int mTotalLatencyMillis;
+        int mNewTypeCount;
+        int mDeletedTypeCount;
+        int mCompatibleTypeChangeCount;
+        int mIndexIncompatibleTypeChangeCount;
+        int mBackwardsIncompatibleTypeChangeCount;
+
+        /** Constructor for the {@link Builder}. */
+        public Builder(@NonNull String packageName, @NonNull String database) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            mDatabase = Preconditions.checkNotNull(database);
+        }
+
+        /** Sets the status of the SetSchema action. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets the status of schema migration. */
+        @NonNull
+        public Builder setSchemaMigrationStats(@NonNull SchemaMigrationStats schemaMigrationStats) {
+            mSchemaMigrationStats = Preconditions.checkNotNull(schemaMigrationStats);
+            return this;
+        }
+
+        /** Sets total latency for the SetSchema action in milliseconds. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /** Sets number of new types. */
+        @NonNull
+        public Builder setNewTypeCount(int newTypeCount) {
+            mNewTypeCount = newTypeCount;
+            return this;
+        }
+
+        /** Sets number of deleted types. */
+        @NonNull
+        public Builder setDeletedTypeCount(int deletedTypeCount) {
+            mDeletedTypeCount = deletedTypeCount;
+            return this;
+        }
+
+        /** Sets number of compatible type changes. */
+        @NonNull
+        public Builder setCompatibleTypeChangeCount(int compatibleTypeChangeCount) {
+            mCompatibleTypeChangeCount = compatibleTypeChangeCount;
+            return this;
+        }
+
+        /** Sets number of index-incompatible type changes. */
+        @NonNull
+        public Builder setIndexIncompatibleTypeChangeCount(int indexIncompatibleTypeChangeCount) {
+            mIndexIncompatibleTypeChangeCount = indexIncompatibleTypeChangeCount;
+            return this;
+        }
+
+        /** Sets number of backwards-incompatible type changes. */
+        @NonNull
+        public Builder setBackwardsIncompatibleTypeChangeCount(
+                int backwardsIncompatibleTypeChangeCount) {
+            mBackwardsIncompatibleTypeChangeCount = backwardsIncompatibleTypeChangeCount;
+            return this;
+        }
+
+        /** Builds a new {@link SetSchemaStats} from the {@link Builder}. */
+        @NonNull
+        public SetSchemaStats build() {
+            return new SetSchemaStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java
index 99f5ae1..f22ceb0 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java
@@ -24,7 +24,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
 
 /**
  * Utilities for working with {@link com.google.common.util.concurrent.ListenableFuture}.
@@ -37,7 +37,7 @@
     /** Executes the given lambda on the given executor and returns a {@link ListenableFuture}. */
     @NonNull
     public static <T> ListenableFuture<T> execute(
-            @NonNull ExecutorService executor,
+            @NonNull Executor executor,
             @NonNull Callable<T> callable) {
         Preconditions.checkNotNull(executor);
         Preconditions.checkNotNull(callable);
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
new file mode 100644
index 0000000..12d4587
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.util;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.exceptions.AppSearchException;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyProto;
+
+/**
+ * Provides utility functions for working with package + database prefixes.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class PrefixUtil {
+    private static final String TAG = "AppSearchPrefixUtil";
+
+    @VisibleForTesting
+    public static final char DATABASE_DELIMITER = '/';
+
+    @VisibleForTesting
+    public static final char PACKAGE_DELIMITER = '$';
+
+    private PrefixUtil() {}
+
+    /**
+     * Creates prefix string for given package name and database name.
+     */
+    @NonNull
+    public static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
+        return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
+    }
+    /**
+     * Creates prefix string for given package name.
+     */
+    @NonNull
+    public static String createPackagePrefix(@NonNull String packageName) {
+        return packageName + PACKAGE_DELIMITER;
+    }
+
+    /**
+     * Returns the package name that's contained within the {@code prefix}.
+     *
+     * @param prefix Prefix string that contains the package name inside of it. The package name
+     *               must be in the front of the string, and separated from the rest of the
+     *               string by the {@link #PACKAGE_DELIMITER}.
+     * @return Valid package name.
+     */
+    @NonNull
+    public static String getPackageName(@NonNull String prefix) {
+        int delimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
+        if (delimiterIndex == -1) {
+            // This should never happen if we construct our prefixes properly
+            Log.wtf(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
+            return "";
+        }
+        return prefix.substring(0, delimiterIndex);
+    }
+
+    /**
+     * Returns the database name that's contained within the {@code prefix}.
+     *
+     * @param prefix Prefix string that contains the database name inside of it. The database name
+     *               must be between the {@link #PACKAGE_DELIMITER} and {@link #DATABASE_DELIMITER}
+     * @return Valid database name.
+     */
+    @NonNull
+    public static String getDatabaseName(@NonNull String prefix) {
+        // TODO (b/184050178) Start database delimiter index search from after package delimiter
+        int packageDelimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
+        int databaseDelimiterIndex = prefix.indexOf(DATABASE_DELIMITER);
+        if (packageDelimiterIndex == -1) {
+            // This should never happen if we construct our prefixes properly
+            Log.wtf(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
+            return "";
+        }
+        if (databaseDelimiterIndex == -1) {
+            // This should never happen if we construct our prefixes properly
+            Log.wtf(TAG, "Malformed prefix doesn't contain database delimiter: " + prefix);
+            return "";
+        }
+        return prefix.substring(packageDelimiterIndex + 1, databaseDelimiterIndex);
+    }
+
+    /**
+     * Creates a string with the package and database prefix removed from the input string.
+     *
+     * @param prefixedString a string containing a package and database prefix.
+     * @return a string with the package and database prefix removed.
+     * @throws AppSearchException if the prefixed value does not contain a valid database name.
+     */
+    @NonNull
+    public static String removePrefix(@NonNull String prefixedString)
+            throws AppSearchException {
+        // The prefix is made up of the package, then the database. So we only need to find the
+        // database cutoff.
+        int delimiterIndex;
+        if ((delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER)) != -1) {
+            // Add 1 to include the char size of the DATABASE_DELIMITER
+            return prefixedString.substring(delimiterIndex + 1);
+        }
+        throw new AppSearchException(
+                AppSearchResult.RESULT_INTERNAL_ERROR,
+                "The prefixed value \"" + prefixedString + "\" doesn't contain a valid "
+                        + "database name");
+    }
+
+    /**
+     * Creates a package and database prefix string from the input string.
+     *
+     * @param prefixedString a string containing a package and database prefix.
+     * @return a string with the package and database prefix
+     * @throws AppSearchException if the prefixed value does not contain a valid database name.
+     */
+    @NonNull
+    public static String getPrefix(@NonNull String prefixedString) throws AppSearchException {
+        int databaseDelimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
+        if (databaseDelimiterIndex == -1) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR,
+                    "The prefixed value \"" + prefixedString + "\" doesn't contain a valid "
+                            + "database name");
+        }
+
+        // Add 1 to include the char size of the DATABASE_DELIMITER
+        return prefixedString.substring(0, databaseDelimiterIndex + 1);
+    }
+
+    /**
+     * Prepends {@code prefix} to all types and namespaces mentioned anywhere in
+     * {@code documentBuilder}.
+     *
+     * @param documentBuilder The document to mutate
+     * @param prefix          The prefix to add
+     */
+    public static void addPrefixToDocument(
+            @NonNull DocumentProto.Builder documentBuilder,
+            @NonNull String prefix) {
+        // Rewrite the type name to include/remove the prefix.
+        String newSchema = prefix + documentBuilder.getSchema();
+        documentBuilder.setSchema(newSchema);
+
+        // Rewrite the namespace to include/remove the prefix.
+        documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
+
+        // Recurse into derived documents
+        for (int propertyIdx = 0;
+                propertyIdx < documentBuilder.getPropertiesCount();
+                propertyIdx++) {
+            int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
+            if (documentCount > 0) {
+                PropertyProto.Builder propertyBuilder =
+                        documentBuilder.getProperties(propertyIdx).toBuilder();
+                for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
+                    DocumentProto.Builder derivedDocumentBuilder =
+                            propertyBuilder.getDocumentValues(documentIdx).toBuilder();
+                    addPrefixToDocument(derivedDocumentBuilder, prefix);
+                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
+                }
+                documentBuilder.setProperties(propertyIdx, propertyBuilder);
+            }
+        }
+    }
+
+    /**
+     * Removes any prefixes from types and namespaces mentioned anywhere in
+     * {@code documentBuilder}.
+     *
+     * @param documentBuilder The document to mutate
+     * @return Prefix name that was removed from the document.
+     * @throws AppSearchException if there are unexpected database prefixing errors.
+     */
+    @NonNull
+    public static String removePrefixesFromDocument(@NonNull DocumentProto.Builder documentBuilder)
+            throws AppSearchException {
+        // Rewrite the type name and namespace to remove the prefix.
+        String schemaPrefix = getPrefix(documentBuilder.getSchema());
+        String namespacePrefix = getPrefix(documentBuilder.getNamespace());
+
+        if (!schemaPrefix.equals(namespacePrefix)) {
+            throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, "Found unexpected"
+                    + " multiple prefix names in document: " + schemaPrefix + ", "
+                    + namespacePrefix);
+        }
+
+        documentBuilder.setSchema(removePrefix(documentBuilder.getSchema()));
+        documentBuilder.setNamespace(removePrefix(documentBuilder.getNamespace()));
+
+        // Recurse into derived documents
+        for (int propertyIdx = 0;
+                propertyIdx < documentBuilder.getPropertiesCount();
+                propertyIdx++) {
+            int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
+            if (documentCount > 0) {
+                PropertyProto.Builder propertyBuilder =
+                        documentBuilder.getProperties(propertyIdx).toBuilder();
+                for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
+                    DocumentProto.Builder derivedDocumentBuilder =
+                            propertyBuilder.getDocumentValues(documentIdx).toBuilder();
+                    String nestedPrefix = removePrefixesFromDocument(derivedDocumentBuilder);
+                    if (!nestedPrefix.equals(schemaPrefix)) {
+                        throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                                "Found unexpected multiple prefix names in document: "
+                                        + schemaPrefix + ", " + nestedPrefix);
+                    }
+                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
+                }
+                documentBuilder.setProperties(propertyIdx, propertyBuilder);
+            }
+        }
+
+        return schemaPrefix;
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
new file mode 100644
index 0000000..d3d754e
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.visibilitystore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.exceptions.AppSearchException;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An interface for classes that store and validate document visibility data.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface VisibilityStore {
+    /**
+     * These cannot have any of the special characters used by AppSearchImpl (e.g. {@code
+     * AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}.
+     */
+    String PACKAGE_NAME = "VS#Pkg";
+
+    @VisibleForTesting
+    String DATABASE_NAME = "VS#Db";
+
+    /**
+     * Sets visibility settings for the given database. Any previous visibility settings will be
+     * overwritten.
+     *
+     * @param packageName Package of app that owns the schemas.
+     * @param databaseName Database that owns the schemas.
+     * @param schemasNotDisplayedBySystem Set of prefixed schemas that should be hidden from
+     *     platform surfaces.
+     * @param schemasVisibleToPackages Map of prefixed schemas to a list of package identifiers that
+     *     have access to the schema.
+     * @throws AppSearchException on AppSearchImpl error.
+     */
+    void setVisibility(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull Set<String> schemasNotDisplayedBySystem,
+            @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
+            throws AppSearchException;
+
+    /**
+     * Checks whether the given package has access to system-surfaceable schemas.
+     *
+     * @param callerUid UID of the app that wants to see the data.
+     */
+    boolean isSchemaSearchableByCaller(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull String prefixedSchema,
+            int callerUid,
+            boolean callerHasSystemAccess);
+}
diff --git a/appsearch/platform-storage/api/current.txt b/appsearch/platform-storage/api/current.txt
new file mode 100644
index 0000000..881789d
--- /dev/null
+++ b/appsearch/platform-storage/api/current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.appsearch.platformstorage {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext {
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext.Builder {
+    ctor public PlatformStorage.GlobalSearchContext.Builder(android.content.Context);
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+  public static final class PlatformStorage.SearchContext {
+    method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.SearchContext.Builder {
+    ctor public PlatformStorage.SearchContext.Builder(android.content.Context, String);
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+}
+
diff --git a/appsearch/platform-storage/api/public_plus_experimental_current.txt b/appsearch/platform-storage/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..881789d
--- /dev/null
+++ b/appsearch/platform-storage/api/public_plus_experimental_current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.appsearch.platformstorage {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext {
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext.Builder {
+    ctor public PlatformStorage.GlobalSearchContext.Builder(android.content.Context);
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+  public static final class PlatformStorage.SearchContext {
+    method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.SearchContext.Builder {
+    ctor public PlatformStorage.SearchContext.Builder(android.content.Context, String);
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+}
+
diff --git a/work/workmanager-gcm/api/res-2.6.0-beta01.txt b/appsearch/platform-storage/api/res-current.txt
similarity index 100%
copy from work/workmanager-gcm/api/res-2.6.0-beta01.txt
copy to appsearch/platform-storage/api/res-current.txt
diff --git a/appsearch/platform-storage/api/restricted_current.txt b/appsearch/platform-storage/api/restricted_current.txt
new file mode 100644
index 0000000..881789d
--- /dev/null
+++ b/appsearch/platform-storage/api/restricted_current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.appsearch.platformstorage {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext {
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext.Builder {
+    ctor public PlatformStorage.GlobalSearchContext.Builder(android.content.Context);
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+  public static final class PlatformStorage.SearchContext {
+    method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.SearchContext.Builder {
+    ctor public PlatformStorage.SearchContext.Builder(android.content.Context, String);
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+}
+
diff --git a/appsearch/platform-storage/build.gradle b/appsearch/platform-storage/build.gradle
new file mode 100644
index 0000000..de527dd6
--- /dev/null
+++ b/appsearch/platform-storage/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    api("androidx.annotation:annotation:1.1.0")
+
+    implementation project(":appsearch:appsearch")
+    implementation("androidx.concurrent:concurrent-futures:1.0.0")
+    implementation("androidx.core:core:1.2.0")
+
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation("junit:junit:4.13")
+}
+
+androidx {
+    name = "AppSearch Platform Storage"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.APPSEARCH
+    mavenVersion = LibraryVersions.APPSEARCH
+    inceptionYear = "2021"
+    description =
+        "An implementation of AppSearchSession which uses the AppSearch service on Android S+"
+}
diff --git a/appsearch/platform-storage/lint-baseline.xml b/appsearch/platform-storage/lint-baseline.xml
new file mode 100644
index 0000000..f23e084
--- /dev/null
+++ b/appsearch/platform-storage/lint-baseline.xml
@@ -0,0 +1,301 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 7.1.0-alpha03" type="baseline" client="gradle" name="AGP (7.1.0-alpha03)" variant="all" version="7.1.0-alpha03">
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+        errorLine1="                platformResult.getResultCode(), platformResult.getErrorMessage());"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java"
+            line="55"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+        errorLine1="                            platformResult.getResultCode(), platformResult.getErrorMessage()));"
+        errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java"
+            line="72"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+        errorLine1="                    failure.getValue().getResultCode(),"
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java"
+            line="99"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+        errorLine1="                                        result.getResultCode(), result.getErrorMessage()));"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java"
+            line="228"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+        errorLine1="                                        result.getResultCode(), result.getErrorMessage()));"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java"
+            line="253"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(stringProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="86"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: StringPropertyConfig.INDEXING_TYPE_NONE, StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS, StringPropertyConfig.INDEXING_TYPE_PREFIXES"
+        errorLine1="                    .setIndexingType(stringProperty.getIndexingType())"
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="87"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: StringPropertyConfig.TOKENIZER_TYPE_NONE, StringPropertyConfig.TOKENIZER_TYPE_PLAIN"
+        errorLine1="                    .setTokenizerType(stringProperty.getTokenizerType())"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="88"
+            column="39"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(jetpackProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="93"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(jetpackProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="98"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(jetpackProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="103"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(jetpackProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="108"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(documentProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="115"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(stringProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="133"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: StringPropertyConfig.INDEXING_TYPE_NONE, StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS, StringPropertyConfig.INDEXING_TYPE_PREFIXES"
+        errorLine1="                    .setIndexingType(stringProperty.getIndexingType())"
+        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="134"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: StringPropertyConfig.TOKENIZER_TYPE_NONE, StringPropertyConfig.TOKENIZER_TYPE_PLAIN"
+        errorLine1="                    .setTokenizerType(stringProperty.getTokenizerType())"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="135"
+            column="39"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(platformProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="140"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(platformProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="145"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(platformProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="150"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(platformProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="155"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: PropertyConfig.CARDINALITY_REPEATED, PropertyConfig.CARDINALITY_OPTIONAL, PropertyConfig.CARDINALITY_REQUIRED"
+        errorLine1="                    .setCardinality(documentProperty.getCardinality())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java"
+            line="164"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+        errorLine1="                        new AppSearchException(result.getResultCode(), result.getErrorMessage()));"
+        errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java"
+            line="71"
+            column="48"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: AppSearchResult.RESULT_OK, AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_INTERNAL_ERROR, AppSearchResult.RESULT_INVALID_ARGUMENT, AppSearchResult.RESULT_IO_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE, AppSearchResult.RESULT_NOT_FOUND, AppSearchResult.RESULT_INVALID_SCHEMA, AppSearchResult.RESULT_SECURITY_ERROR"
+        errorLine1="                        platformResult.getResultCode(), platformResult.getErrorMessage()));"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java"
+            line="258"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: SearchSpec.TERM_MATCH_EXACT_ONLY, SearchSpec.TERM_MATCH_PREFIX"
+        errorLine1="                .setTermMatch(jetpackSearchSpec.getTermMatch())"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"
+            line="49"
+            column="31"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: SearchSpec.RANKING_STRATEGY_NONE, SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE, SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP, SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE, SearchSpec.RANKING_STRATEGY_USAGE_COUNT, SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP, SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT, SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP"
+        errorLine1="                .setRankingStrategy(jetpackSearchSpec.getRankingStrategy())"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"
+            line="54"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one of: SearchSpec.ORDER_DESCENDING, SearchSpec.ORDER_ASCENDING"
+        errorLine1="                .setOrder(jetpackSearchSpec.getOrder())"
+        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"
+            line="55"
+            column="27"/>
+    </issue>
+
+    <issue
+        id="WrongConstant"
+        message="Must be one or more of: SearchSpec.GROUPING_TYPE_PER_PACKAGE, SearchSpec.GROUPING_TYPE_PER_NAMESPACE"
+        errorLine1="                    jetpackSearchSpec.getResultGroupingTypeFlags(),"
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java"
+            line="61"
+            column="21"/>
+    </issue>
+
+</issues>
diff --git a/appsearch/platform-storage/src/androidTest/AndroidManifest.xml b/appsearch/platform-storage/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..06cf077
--- /dev/null
+++ b/appsearch/platform-storage/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright (C) 2019 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.appsearch.platformstorage.test">
+</manifest>
diff --git a/appsearch/platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/PlatformStorageTest.java b/appsearch/platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/PlatformStorageTest.java
new file mode 100644
index 0000000..3adaf9c
--- /dev/null
+++ b/appsearch/platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/PlatformStorageTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.platformstorage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class PlatformStorageTest {
+    @Test
+    public void testSearchContext_databaseName() {
+        PlatformStorage.SearchContext searchContext =
+                new PlatformStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        /*databaseName=*/"dbName").build();
+
+        assertThat(searchContext.getDatabaseName()).isEqualTo("dbName");
+    }
+
+    @Test
+    public void testSearchContext_withClientExecutor() {
+        Executor executor = Executors.newSingleThreadExecutor();
+        PlatformStorage.SearchContext searchContext = new PlatformStorage.SearchContext.Builder(
+                ApplicationProvider.getApplicationContext(),
+                /*databaseName=*/"dbName")
+                .setWorkerExecutor(executor)
+                .build();
+
+        assertThat(searchContext.getWorkerExecutor()).isEqualTo(executor);
+        assertThat(searchContext.getDatabaseName()).isEqualTo("dbName");
+    }
+
+    @Test
+    public void testSearchContext_withDefaultExecutor() {
+        PlatformStorage.SearchContext searchContext = new PlatformStorage.SearchContext.Builder(
+                ApplicationProvider.getApplicationContext(),
+                /*databaseName=*/"dbName")
+                .build();
+
+        assertThat(searchContext.getWorkerExecutor()).isNotNull();
+        assertThat(searchContext.getDatabaseName()).isEqualTo("dbName");
+    }
+
+    @Test
+    public void testSearchContext_withInvalidDatabaseName() {
+        // Test special character can present in database name. When a special character is banned
+        // in database name, add checker in SearchContext.Builder and reflect it in java doc.
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new PlatformStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        "testDatabaseNameEndWith/").build());
+        assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
+        e = assertThrows(IllegalArgumentException.class,
+                () -> new PlatformStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        "/testDatabaseNameStartWith").build());
+        assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
+    }
+}
diff --git a/appsearch/platform-storage/src/main/AndroidManifest.xml b/appsearch/platform-storage/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3cd15c8
--- /dev/null
+++ b/appsearch/platform-storage/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2021 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.
+-->
+
+<manifest package="androidx.appsearch.platformstorage"/>
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
new file mode 100644
index 0000000..d5e9972
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.app.ReportSystemUsageRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link androidx.appsearch.app.GlobalSearchSession} which proxies to a
+ * platform {@link android.app.appsearch.GlobalSearchSession}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+class GlobalSearchSessionImpl implements GlobalSearchSession {
+    private final android.app.appsearch.GlobalSearchSession mPlatformSession;
+    private final Executor mExecutor;
+
+    GlobalSearchSessionImpl(
+            @NonNull android.app.appsearch.GlobalSearchSession platformSession,
+            @NonNull Executor executor) {
+        mPlatformSession = Preconditions.checkNotNull(platformSession);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    @Override
+    @NonNull
+    public SearchResults search(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        android.app.appsearch.SearchResults platformSearchResults =
+                mPlatformSession.search(
+                        queryExpression,
+                        SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));
+        return new SearchResultsImpl(platformSearchResults, mExecutor);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<Void> future = ResolvableFuture.create();
+        mPlatformSession.reportSystemUsage(
+                RequestToPlatformConverter.toPlatformReportSystemUsageRequest(request),
+                mExecutor,
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result, future));
+        return future;
+    }
+
+    @Override
+    public void close() {
+        mPlatformSession.close();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
new file mode 100644
index 0000000..b07782b
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.app.appsearch.AppSearchManager;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.platformstorage.converter.SearchContextToPlatformConverter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * An AppSearch storage system which stores data in the central AppSearch service, available on
+ * Android S+.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public final class PlatformStorage {
+
+    private PlatformStorage() {
+    }
+
+    /** Contains information about how to create the search session. */
+    public static final class SearchContext {
+        final Context mContext;
+        final String mDatabaseName;
+        final Executor mExecutor;
+
+        SearchContext(@NonNull Context context, @NonNull String databaseName,
+                @NonNull Executor executor) {
+            mContext = Preconditions.checkNotNull(context);
+            mDatabaseName = Preconditions.checkNotNull(databaseName);
+            mExecutor = Preconditions.checkNotNull(executor);
+        }
+
+        /**
+         * Returns the name of the database to create or open.
+         */
+        @NonNull
+        public String getDatabaseName() {
+            return mDatabaseName;
+        }
+
+        /**
+         * Returns the worker executor associated with {@link AppSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
+        }
+
+        /** Builder for {@link SearchContext} objects. */
+        public static final class Builder {
+            private final Context mContext;
+            private final String mDatabaseName;
+            private Executor mExecutor;
+
+            /**
+             * Creates a {@link SearchContext.Builder} instance.
+             *
+             * <p>{@link AppSearchSession} will create or open a database under the given name.
+             *
+             * <p>Databases with different names are fully separate with distinct schema types,
+             * namespaces, and documents.
+             *
+             * <p>The database name cannot contain {@code '/'}.
+             *
+             * <p>The database name will be visible to all system UI or third-party applications
+             * that have been granted access to any of the database's documents (for example,
+             * using {@link
+             * androidx.appsearch.app.SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}).
+             *
+             * @param databaseName The name of the database.
+             * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
+             */
+            public Builder(@NonNull Context context, @NonNull String databaseName) {
+                mContext = Preconditions.checkNotNull(context);
+                Preconditions.checkNotNull(databaseName);
+                if (databaseName.contains("/")) {
+                    throw new IllegalArgumentException("Database name cannot contain '/'");
+                }
+                mDatabaseName = databaseName;
+            }
+
+            /**
+             * Sets the worker executor associated with {@link AppSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                mExecutor = Preconditions.checkNotNull(executor);
+                return this;
+            }
+
+            /** Builds a {@link SearchContext} instance. */
+            @NonNull
+            public SearchContext build() {
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
+                return new SearchContext(mContext, mDatabaseName, mExecutor);
+            }
+        }
+    }
+
+    /** Contains information relevant to creating a global search session. */
+    public static final class GlobalSearchContext {
+        final Context mContext;
+        final Executor mExecutor;
+
+        GlobalSearchContext(@NonNull Context context, @NonNull Executor executor) {
+            mContext = Preconditions.checkNotNull(context);
+            mExecutor = Preconditions.checkNotNull(executor);
+        }
+
+        /**
+         * Returns the worker executor associated with {@link GlobalSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
+        }
+
+        /** Builder for {@link GlobalSearchContext} objects. */
+        public static final class Builder {
+            private final Context mContext;
+            private Executor mExecutor;
+
+            public Builder(@NonNull Context context) {
+                mContext = Preconditions.checkNotNull(context);
+            }
+
+            /**
+             * Sets the worker executor associated with {@link GlobalSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                Preconditions.checkNotNull(executor);
+                mExecutor = executor;
+                return this;
+            }
+
+            /** Builds a {@link GlobalSearchContext} instance. */
+            @NonNull
+            public GlobalSearchContext build() {
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
+                return new GlobalSearchContext(mContext, mExecutor);
+            }
+        }
+    }
+
+    // Never call Executor.shutdownNow(), it will cancel the futures it's returned. And since
+    // execute() won't return anything, we will hang forever waiting for the execution.
+    // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
+    // mutate requests will need to gain write lock and query requests need to gain read lock.
+    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+
+    /**
+     * Opens a new {@link AppSearchSession} on this storage.
+     *
+     * @param context The {@link SearchContext} contains all information to create a new
+     *                {@link AppSearchSession}
+     */
+    @NonNull
+    public static ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull SearchContext context) {
+        Preconditions.checkNotNull(context);
+        AppSearchManager appSearchManager =
+                context.mContext.getSystemService(AppSearchManager.class);
+        ResolvableFuture<AppSearchSession> future = ResolvableFuture.create();
+        appSearchManager.createSearchSession(
+                SearchContextToPlatformConverter.toPlatformSearchContext(context),
+                context.mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.set(
+                                new SearchSessionImpl(result.getResultValue(), context.mExecutor));
+                    } else {
+                        future.setException(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
+                    }
+                });
+        return future;
+    }
+
+    /**
+     * Opens a new {@link GlobalSearchSession} on this storage.
+     */
+    @NonNull
+    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+            @NonNull GlobalSearchContext context) {
+        Preconditions.checkNotNull(context);
+        AppSearchManager appSearchManager =
+                context.mContext.getSystemService(AppSearchManager.class);
+        ResolvableFuture<GlobalSearchSession> future = ResolvableFuture.create();
+        appSearchManager.createGlobalSearchSession(
+                context.mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.set(new GlobalSearchSessionImpl(
+                                result.getResultValue(), context.mExecutor));
+                    } else {
+                        future.setException(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
+                    }
+                });
+        return future;
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
new file mode 100644
index 0000000..52b15cf3
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.platformstorage.converter.SearchResultToPlatformConverter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Platform implementation of {@link SearchResults} which proxies to the platform's
+ * {@link android.app.appsearch.SearchResults}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+class SearchResultsImpl implements SearchResults {
+    private final android.app.appsearch.SearchResults mPlatformResults;
+    private final Executor mExecutor;
+
+    SearchResultsImpl(
+            @NonNull android.app.appsearch.SearchResults platformResults,
+            @NonNull Executor executor) {
+        mPlatformResults = Preconditions.checkNotNull(platformResults);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<List<SearchResult>> getNextPage() {
+        ResolvableFuture<List<SearchResult>> future = ResolvableFuture.create();
+        mPlatformResults.getNextPage(mExecutor, result -> {
+            if (result.isSuccess()) {
+                List<android.app.appsearch.SearchResult> frameworkResults = result.getResultValue();
+                List<SearchResult> jetpackResults = new ArrayList<>(frameworkResults.size());
+                for (int i = 0; i < frameworkResults.size(); i++) {
+                    SearchResult jetpackResult =
+                            SearchResultToPlatformConverter.toJetpackSearchResult(
+                                    frameworkResults.get(i));
+                    jetpackResults.add(jetpackResult);
+                }
+                future.set(jetpackResults);
+            } else {
+                future.setException(
+                        new AppSearchException(result.getResultCode(), result.getErrorMessage()));
+            }
+        });
+        return future;
+    }
+
+    @Override
+    public void close() {
+        mPlatformResults.close();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
new file mode 100644
index 0000000..8cbf935d
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.ResponseToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SchemaToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SetSchemaRequestToPlatformConverter;
+import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link AppSearchSession} which proxies to a platform
+ * {@link android.app.appsearch.AppSearchSession}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+class SearchSessionImpl implements AppSearchSession {
+    private final android.app.appsearch.AppSearchSession mPlatformSession;
+    private final Executor mExecutor;
+
+    SearchSessionImpl(
+            @NonNull android.app.appsearch.AppSearchSession platformSession,
+            @NonNull Executor executor) {
+        mPlatformSession = Preconditions.checkNotNull(platformSession);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<SetSchemaResponse> future = ResolvableFuture.create();
+        mPlatformSession.setSchema(
+                SetSchemaRequestToPlatformConverter.toPlatformSetSchemaRequest(request),
+                mExecutor,
+                mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        SetSchemaResponse jetpackResponse =
+                                SetSchemaRequestToPlatformConverter.toJetpackSetSchemaResponse(
+                                        result.getResultValue());
+                        future.set(jetpackResponse);
+                    } else {
+                        handleFailedPlatformResult(result, future);
+                    }
+                });
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<GetSchemaResponse> getSchema() {
+        ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
+        mPlatformSession.getSchema(
+                mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        android.app.appsearch.GetSchemaResponse platformGetResponse =
+                                result.getResultValue();
+                        GetSchemaResponse.Builder jetpackResponseBuilder =
+                                new GetSchemaResponse.Builder();
+                        for (android.app.appsearch.AppSearchSchema platformSchema :
+                                platformGetResponse.getSchemas()) {
+                            jetpackResponseBuilder.addSchema(
+                                    SchemaToPlatformConverter.toJetpackSchema(platformSchema));
+                        }
+                        jetpackResponseBuilder.setVersion(platformGetResponse.getVersion());
+                        future.set(jetpackResponseBuilder.build());
+                    } else {
+                        handleFailedPlatformResult(result, future);
+                    }
+                });
+        return future;
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Set<String>> getNamespaces() {
+        ResolvableFuture<Set<String>> future = ResolvableFuture.create();
+        mPlatformSession.getNamespaces(
+                mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.set(result.getResultValue());
+                    } else {
+                        handleFailedPlatformResult(result, future);
+                    }
+                });
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, Void>> put(
+            @NonNull PutDocumentsRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
+        mPlatformSession.put(
+                RequestToPlatformConverter.toPlatformPutDocumentsRequest(request),
+                mExecutor,
+                BatchResultCallbackAdapter.forSameValueType(future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
+                ResolvableFuture.create();
+        mPlatformSession.getByDocumentId(
+                RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request),
+                mExecutor,
+                new BatchResultCallbackAdapter<>(
+                        future, GenericDocumentToPlatformConverter::toJetpackGenericDocument));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public SearchResults search(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        android.app.appsearch.SearchResults platformSearchResults =
+                mPlatformSession.search(
+                        queryExpression,
+                        SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));
+        return new SearchResultsImpl(platformSearchResults, mExecutor);
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<Void> future = ResolvableFuture.create();
+        mPlatformSession.reportUsage(
+                RequestToPlatformConverter.toPlatformReportUsageRequest(request),
+                mExecutor,
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result, future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
+        mPlatformSession.remove(
+                RequestToPlatformConverter.toPlatformRemoveByDocumentIdRequest(request),
+                mExecutor,
+                BatchResultCallbackAdapter.forSameValueType(future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<Void> remove(
+            @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        ResolvableFuture<Void> future = ResolvableFuture.create();
+        mPlatformSession.remove(
+                queryExpression,
+                SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec),
+                mExecutor,
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result, future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<StorageInfo> getStorageInfo() {
+        ResolvableFuture<StorageInfo> future = ResolvableFuture.create();
+        mPlatformSession.getStorageInfo(
+                mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        StorageInfo jetpackStorageInfo =
+                                ResponseToPlatformConverter.toJetpackStorageInfo(
+                                        result.getResultValue());
+                        future.set(jetpackStorageInfo);
+                    } else {
+                        handleFailedPlatformResult(result, future);
+                    }
+                });
+        return future;
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> requestFlush() {
+        ResolvableFuture<Void> future = ResolvableFuture.create();
+        // The data in platform will be flushed by scheduled task. This api won't do anything extra
+        // flush.
+        future.set(null);
+        return future;
+    }
+
+    @Override
+    public void close() {
+        mPlatformSession.close();
+    }
+
+    private void handleFailedPlatformResult(
+            @NonNull android.app.appsearch.AppSearchResult<?> platformResult,
+            @NonNull ResolvableFuture<?> future) {
+        future.setException(
+                new AppSearchException(
+                        platformResult.getResultCode(), platformResult.getErrorMessage()));
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
new file mode 100644
index 0000000..1510626
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Translates {@link androidx.appsearch.app.AppSearchResult} and
+ * {@link androidx.appsearch.app.AppSearchBatchResult} to platform versions.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class AppSearchResultToPlatformConverter {
+    private AppSearchResultToPlatformConverter() {}
+
+    /**
+     * Converts an {@link android.app.appsearch.AppSearchResult} into a jetpack
+     * {@link androidx.appsearch.app.AppSearchResult}.
+     */
+    @NonNull
+    public static <T> AppSearchResult<T> platformAppSearchResultToJetpack(
+            @NonNull android.app.appsearch.AppSearchResult<T> platformResult) {
+        Preconditions.checkNotNull(platformResult);
+        if (platformResult.isSuccess()) {
+            return AppSearchResult.newSuccessfulResult(platformResult.getResultValue());
+        }
+        return AppSearchResult.newFailedResult(
+                platformResult.getResultCode(), platformResult.getErrorMessage());
+    }
+
+    /**
+     * Uses the given {@link android.app.appsearch.AppSearchResult} to populate the given
+     * {@link ResolvableFuture}.
+     */
+    public static <T> void platformAppSearchResultToFuture(
+            @NonNull android.app.appsearch.AppSearchResult<T> platformResult,
+            @NonNull ResolvableFuture<T> future) {
+        Preconditions.checkNotNull(platformResult);
+        Preconditions.checkNotNull(future);
+        if (platformResult.isSuccess()) {
+            future.set(platformResult.getResultValue());
+        } else {
+            future.setException(
+                    new AppSearchException(
+                            platformResult.getResultCode(), platformResult.getErrorMessage()));
+        }
+    }
+
+    /**
+     * Converts the given platform {@link android.app.appsearch.AppSearchBatchResult} to a Jetpack
+     * {@link AppSearchBatchResult}.
+     *
+     * <p>Each value is translated using the provided {@code valueMapper} function.
+     */
+    @NonNull
+    public static <K, PlatformValue, JetpackValue> AppSearchBatchResult<K, JetpackValue>
+            platformAppSearchBatchResultToJetpack(
+            @NonNull android.app.appsearch.AppSearchBatchResult<K, PlatformValue> platformResult,
+            @NonNull Function<PlatformValue, JetpackValue> valueMapper) {
+        Preconditions.checkNotNull(platformResult);
+        Preconditions.checkNotNull(valueMapper);
+        AppSearchBatchResult.Builder<K, JetpackValue> jetpackResult =
+                new AppSearchBatchResult.Builder<>();
+        for (Map.Entry<K, PlatformValue> success : platformResult.getSuccesses().entrySet()) {
+            JetpackValue jetpackValue = valueMapper.apply(success.getValue());
+            jetpackResult.setSuccess(success.getKey(), jetpackValue);
+        }
+        for (Map.Entry<K, android.app.appsearch.AppSearchResult<PlatformValue>> failure :
+                platformResult.getFailures().entrySet()) {
+            jetpackResult.setFailure(
+                    failure.getKey(),
+                    failure.getValue().getResultCode(),
+                    failure.getValue().getErrorMessage());
+        }
+        return jetpackResult.build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
new file mode 100644
index 0000000..3838191
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link GenericDocument}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class GenericDocumentToPlatformConverter {
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.GenericDocument} into a platform
+     * {@link android.app.appsearch.GenericDocument}.
+     */
+    @NonNull
+    public static android.app.appsearch.GenericDocument toPlatformGenericDocument(
+            @NonNull GenericDocument jetpackDocument) {
+        Preconditions.checkNotNull(jetpackDocument);
+        android.app.appsearch.GenericDocument.Builder<
+                android.app.appsearch.GenericDocument.Builder<?>> platformBuilder =
+                new android.app.appsearch.GenericDocument.Builder<>(
+                        jetpackDocument.getNamespace(),
+                        jetpackDocument.getId(),
+                        jetpackDocument.getSchemaType());
+        platformBuilder
+                .setScore(jetpackDocument.getScore())
+                .setTtlMillis(jetpackDocument.getTtlMillis())
+                .setCreationTimestampMillis(jetpackDocument.getCreationTimestampMillis());
+        for (String propertyName : jetpackDocument.getPropertyNames()) {
+            Object property = jetpackDocument.getProperty(propertyName);
+            if (property instanceof String[]) {
+                platformBuilder.setPropertyString(propertyName, (String[]) property);
+            } else if (property instanceof long[]) {
+                platformBuilder.setPropertyLong(propertyName, (long[]) property);
+            } else if (property instanceof double[]) {
+                platformBuilder.setPropertyDouble(propertyName, (double[]) property);
+            } else if (property instanceof boolean[]) {
+                platformBuilder.setPropertyBoolean(propertyName, (boolean[]) property);
+            } else if (property instanceof byte[][]) {
+                platformBuilder.setPropertyBytes(propertyName, (byte[][]) property);
+            } else if (property instanceof GenericDocument[]) {
+                GenericDocument[] documentValues = (GenericDocument[]) property;
+                android.app.appsearch.GenericDocument[] platformSubDocuments =
+                        new android.app.appsearch.GenericDocument[documentValues.length];
+                for (int j = 0; j < documentValues.length; j++) {
+                    platformSubDocuments[j] = toPlatformGenericDocument(documentValues[j]);
+                }
+                platformBuilder.setPropertyDocument(propertyName, platformSubDocuments);
+            } else {
+                throw new IllegalStateException(
+                        String.format("Property \"%s\" has unsupported value type %s", propertyName,
+                                property.getClass().toString()));
+            }
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a platform {@link android.app.appsearch.GenericDocument} into a jetpack
+     * {@link androidx.appsearch.app.GenericDocument}.
+     */
+    @NonNull
+    public static GenericDocument toJetpackGenericDocument(
+            @NonNull android.app.appsearch.GenericDocument platformDocument) {
+        Preconditions.checkNotNull(platformDocument);
+        GenericDocument.Builder<GenericDocument.Builder<?>> jetpackBuilder =
+                new GenericDocument.Builder<>(
+                        platformDocument.getNamespace(),
+                        platformDocument.getId(),
+                        platformDocument.getSchemaType());
+        jetpackBuilder
+                .setScore(platformDocument.getScore())
+                .setTtlMillis(platformDocument.getTtlMillis())
+                .setCreationTimestampMillis(platformDocument.getCreationTimestampMillis());
+        for (String propertyName : platformDocument.getPropertyNames()) {
+            Object property = platformDocument.getProperty(propertyName);
+            if (property instanceof String[]) {
+                jetpackBuilder.setPropertyString(propertyName, (String[]) property);
+            } else if (property instanceof long[]) {
+                jetpackBuilder.setPropertyLong(propertyName, (long[]) property);
+            } else if (property instanceof double[]) {
+                jetpackBuilder.setPropertyDouble(propertyName, (double[]) property);
+            } else if (property instanceof boolean[]) {
+                jetpackBuilder.setPropertyBoolean(propertyName, (boolean[]) property);
+            } else if (property instanceof byte[][]) {
+                jetpackBuilder.setPropertyBytes(propertyName, (byte[][]) property);
+            } else if (property instanceof android.app.appsearch.GenericDocument[]) {
+                android.app.appsearch.GenericDocument[] documentValues =
+                        (android.app.appsearch.GenericDocument[]) property;
+                GenericDocument[] jetpackSubDocuments = new GenericDocument[documentValues.length];
+                for (int j = 0; j < documentValues.length; j++) {
+                    jetpackSubDocuments[j] = toJetpackGenericDocument(documentValues[j]);
+                }
+                jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
+            } else {
+                throw new IllegalStateException(
+                        String.format("Property \"%s\" has unsupported value type %s", propertyName,
+                                property.getClass().toString()));
+            }
+        }
+        return jetpackBuilder.build();
+    }
+
+    private GenericDocumentToPlatformConverter() {}
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
new file mode 100644
index 0000000..d2c35bd
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportSystemUsageRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates between Platform and Jetpack versions of requests.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class RequestToPlatformConverter {
+    private RequestToPlatformConverter() {}
+
+    /**
+     * Translates a jetpack {@link PutDocumentsRequest} into a platform
+     * {@link android.app.appsearch.PutDocumentsRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.PutDocumentsRequest toPlatformPutDocumentsRequest(
+            @NonNull PutDocumentsRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        android.app.appsearch.PutDocumentsRequest.Builder platformBuilder =
+                new android.app.appsearch.PutDocumentsRequest.Builder();
+        for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) {
+            platformBuilder.addGenericDocuments(
+                    GenericDocumentToPlatformConverter.toPlatformGenericDocument(jetpackDocument));
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a jetpack {@link GetByDocumentIdRequest} into a platform
+     * {@link android.app.appsearch.GetByDocumentIdRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.GetByDocumentIdRequest toPlatformGetByDocumentIdRequest(
+            @NonNull GetByDocumentIdRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        android.app.appsearch.GetByDocumentIdRequest.Builder platformBuilder =
+                new android.app.appsearch.GetByDocumentIdRequest.Builder(
+                        jetpackRequest.getNamespace())
+                        .addIds(jetpackRequest.getIds());
+        for (Map.Entry<String, List<String>> projection :
+                jetpackRequest.getProjectionsInternal().entrySet()) {
+            platformBuilder.addProjection(projection.getKey(), projection.getValue());
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a jetpack {@link RemoveByDocumentIdRequest} into a platform
+     * {@link android.app.appsearch.RemoveByDocumentIdRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.RemoveByDocumentIdRequest
+            toPlatformRemoveByDocumentIdRequest(
+            @NonNull RemoveByDocumentIdRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        return new android.app.appsearch.RemoveByDocumentIdRequest.Builder(
+                jetpackRequest.getNamespace())
+                .addIds(jetpackRequest.getIds())
+                .build();
+    }
+
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.ReportUsageRequest} into a platform
+     * {@link android.app.appsearch.ReportUsageRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.ReportUsageRequest toPlatformReportUsageRequest(
+            @NonNull ReportUsageRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        return new android.app.appsearch.ReportUsageRequest.Builder(
+                jetpackRequest.getNamespace(), jetpackRequest.getDocumentId())
+                .setUsageTimestampMillis(jetpackRequest.getUsageTimestampMillis())
+                .build();
+    }
+
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.ReportSystemUsageRequest} into a platform
+     * {@link android.app.appsearch.ReportSystemUsageRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.ReportSystemUsageRequest toPlatformReportSystemUsageRequest(
+            @NonNull ReportSystemUsageRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        return new android.app.appsearch.ReportSystemUsageRequest.Builder(
+                jetpackRequest.getPackageName(),
+                jetpackRequest.getDatabaseName(),
+                jetpackRequest.getNamespace(),
+                jetpackRequest.getDocumentId())
+                .setUsageTimestampMillis(jetpackRequest.getUsageTimestampMillis())
+                .build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ResponseToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ResponseToPlatformConverter.java
new file mode 100644
index 0000000..e6f1218
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ResponseToPlatformConverter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.StorageInfo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Platform and Jetpack versions of responses.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class ResponseToPlatformConverter {
+    private ResponseToPlatformConverter() {}
+
+    /**
+     * Translates a platform {@link android.app.appsearch.StorageInfo} into a jetpack
+     * {@link StorageInfo}.
+     */
+    @NonNull
+    public static StorageInfo toJetpackStorageInfo(
+            @NonNull android.app.appsearch.StorageInfo platformStorageInfo) {
+        Preconditions.checkNotNull(platformStorageInfo);
+        return new StorageInfo.Builder()
+                .setAliveNamespacesCount(platformStorageInfo.getAliveNamespacesCount())
+                .setAliveDocumentsCount(platformStorageInfo.getAliveDocumentsCount())
+                .setSizeBytes(platformStorageInfo.getSizeBytes())
+                .build();
+
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
new file mode 100644
index 0000000..d33c3f9
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Translates a jetpack {@link androidx.appsearch.app.AppSearchSchema} into a platform
+ * {@link android.app.appsearch.AppSearchSchema}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class SchemaToPlatformConverter {
+    private SchemaToPlatformConverter() {}
+
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.AppSearchSchema} into a platform
+     * {@link android.app.appsearch.AppSearchSchema}.
+     */
+    @NonNull
+    public static android.app.appsearch.AppSearchSchema toPlatformSchema(
+            @NonNull AppSearchSchema jetpackSchema) {
+        Preconditions.checkNotNull(jetpackSchema);
+        android.app.appsearch.AppSearchSchema.Builder platformBuilder =
+                new android.app.appsearch.AppSearchSchema.Builder(jetpackSchema.getSchemaType());
+        List<AppSearchSchema.PropertyConfig> properties = jetpackSchema.getProperties();
+        for (int i = 0; i < properties.size(); i++) {
+            android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty =
+                    toPlatformProperty(properties.get(i));
+            platformBuilder.addProperty(platformProperty);
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a platform {@link android.app.appsearch.AppSearchSchema} to a jetpack
+     * {@link androidx.appsearch.app.AppSearchSchema}.
+     */
+    @NonNull
+    public static AppSearchSchema toJetpackSchema(
+            @NonNull android.app.appsearch.AppSearchSchema platformSchema) {
+        Preconditions.checkNotNull(platformSchema);
+        AppSearchSchema.Builder jetpackBuilder =
+                new AppSearchSchema.Builder(platformSchema.getSchemaType());
+        List<android.app.appsearch.AppSearchSchema.PropertyConfig> properties =
+                platformSchema.getProperties();
+        for (int i = 0; i < properties.size(); i++) {
+            AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i));
+            jetpackBuilder.addProperty(jetpackProperty);
+        }
+        return jetpackBuilder.build();
+    }
+
+    @NonNull
+    private static android.app.appsearch.AppSearchSchema.PropertyConfig toPlatformProperty(
+            @NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
+        Preconditions.checkNotNull(jetpackProperty);
+        if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
+            AppSearchSchema.StringPropertyConfig stringProperty =
+                    (AppSearchSchema.StringPropertyConfig) jetpackProperty;
+            return new android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder(
+                    stringProperty.getName())
+                    .setCardinality(stringProperty.getCardinality())
+                    .setIndexingType(stringProperty.getIndexingType())
+                    .setTokenizerType(stringProperty.getTokenizerType())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.LongPropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.DoublePropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.DoublePropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.BooleanPropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.BooleanPropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.BytesPropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.BytesPropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.DocumentPropertyConfig) {
+            AppSearchSchema.DocumentPropertyConfig documentProperty =
+                    (AppSearchSchema.DocumentPropertyConfig) jetpackProperty;
+            return new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
+                    documentProperty.getName(), documentProperty.getSchemaType())
+                    .setCardinality(documentProperty.getCardinality())
+                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
+                    .build();
+        } else {
+            throw new IllegalArgumentException(
+                    "Invalid dataType: " + jetpackProperty.getDataType());
+        }
+    }
+
+    @NonNull
+    private static AppSearchSchema.PropertyConfig toJetpackProperty(
+            @NonNull android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty) {
+        Preconditions.checkNotNull(platformProperty);
+        if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.StringPropertyConfig) {
+            android.app.appsearch.AppSearchSchema.StringPropertyConfig stringProperty =
+                    (android.app.appsearch.AppSearchSchema.StringPropertyConfig) platformProperty;
+            return new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName())
+                    .setCardinality(stringProperty.getCardinality())
+                    .setIndexingType(stringProperty.getIndexingType())
+                    .setTokenizerType(stringProperty.getTokenizerType())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.LongPropertyConfig) {
+            return new AppSearchSchema.LongPropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.DoublePropertyConfig) {
+            return new AppSearchSchema.DoublePropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+            return new AppSearchSchema.BooleanPropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.BytesPropertyConfig) {
+            return new AppSearchSchema.BytesPropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) {
+            android.app.appsearch.AppSearchSchema.DocumentPropertyConfig documentProperty =
+                    (android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) platformProperty;
+            return new AppSearchSchema.DocumentPropertyConfig.Builder(
+                    documentProperty.getName(),
+                    documentProperty.getSchemaType())
+                    .setCardinality(documentProperty.getCardinality())
+                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
+                    .build();
+        } else {
+            throw new IllegalArgumentException(
+                    "Invalid property type " + platformProperty.getClass()
+                            + ": " + platformProperty);
+        }
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchContextToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchContextToPlatformConverter.java
new file mode 100644
index 0000000..49b0564
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchContextToPlatformConverter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.app.appsearch.AppSearchManager;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates a Jetpack {@link androidx.appsearch.platformstorage.PlatformStorage.SearchContext}
+ * into a platform {@link android.app.appsearch.AppSearchManager.SearchContext}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class SearchContextToPlatformConverter {
+    private SearchContextToPlatformConverter() {}
+
+    /**
+     * Translates a Jetpack {@link androidx.appsearch.platformstorage.PlatformStorage.SearchContext}
+     * into a platform {@link android.app.appsearch.AppSearchManager.SearchContext}.
+     */
+    @NonNull
+    public static AppSearchManager.SearchContext toPlatformSearchContext(
+            @NonNull PlatformStorage.SearchContext jetpackSearchContext) {
+        Preconditions.checkNotNull(jetpackSearchContext);
+        return new AppSearchManager.SearchContext.Builder(jetpackSearchContext.getDatabaseName())
+                .build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
new file mode 100644
index 0000000..fc7b70e
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResult;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link SearchResult}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public class SearchResultToPlatformConverter {
+    private SearchResultToPlatformConverter() {}
+
+    /** Translates from Platform to Jetpack versions of {@link SearchResult}. */
+    @NonNull
+    public static SearchResult toJetpackSearchResult(
+            @NonNull android.app.appsearch.SearchResult platformResult) {
+        Preconditions.checkNotNull(platformResult);
+        GenericDocument document = GenericDocumentToPlatformConverter.toJetpackGenericDocument(
+                platformResult.getGenericDocument());
+        SearchResult.Builder builder = new SearchResult.Builder(
+                platformResult.getPackageName(), platformResult.getDatabaseName())
+                .setGenericDocument(document)
+                .setRankingSignal(platformResult.getRankingSignal());
+        List<android.app.appsearch.SearchResult.MatchInfo> platformMatches =
+                platformResult.getMatchInfos();
+        for (int i = 0; i < platformMatches.size(); i++) {
+            SearchResult.MatchInfo jetpackMatchInfo = toJetpackMatchInfo(platformMatches.get(i));
+            builder.addMatchInfo(jetpackMatchInfo);
+        }
+        return builder.build();
+    }
+
+    @NonNull
+    private static SearchResult.MatchInfo toJetpackMatchInfo(
+            @NonNull android.app.appsearch.SearchResult.MatchInfo platformMatchInfo) {
+        Preconditions.checkNotNull(platformMatchInfo);
+        return new SearchResult.MatchInfo.Builder(platformMatchInfo.getPropertyPath())
+                .setExactMatchRange(
+                        new SearchResult.MatchRange(
+                                platformMatchInfo.getExactMatchRange().getStart(),
+                                platformMatchInfo.getExactMatchRange().getEnd()))
+                .setSnippetRange(
+                        new SearchResult.MatchRange(
+                                platformMatchInfo.getSnippetRange().getStart(),
+                                platformMatchInfo.getSnippetRange().getEnd()))
+                .build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
new file mode 100644
index 0000000..eadc109
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSpec;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link SearchSpec}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class SearchSpecToPlatformConverter {
+    private SearchSpecToPlatformConverter() {
+    }
+
+    /** Translates from Jetpack to Platform version of {@link SearchSpec}. */
+    @NonNull
+    public static android.app.appsearch.SearchSpec toPlatformSearchSpec(
+            @NonNull SearchSpec jetpackSearchSpec) {
+        Preconditions.checkNotNull(jetpackSearchSpec);
+        android.app.appsearch.SearchSpec.Builder platformBuilder =
+                new android.app.appsearch.SearchSpec.Builder();
+        platformBuilder
+                .setTermMatch(jetpackSearchSpec.getTermMatch())
+                .addFilterSchemas(jetpackSearchSpec.getFilterSchemas())
+                .addFilterNamespaces(jetpackSearchSpec.getFilterNamespaces())
+                .addFilterPackageNames(jetpackSearchSpec.getFilterPackageNames())
+                .setResultCountPerPage(jetpackSearchSpec.getResultCountPerPage())
+                .setRankingStrategy(jetpackSearchSpec.getRankingStrategy())
+                .setOrder(jetpackSearchSpec.getOrder())
+                .setSnippetCount(jetpackSearchSpec.getSnippetCount())
+                .setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
+                .setMaxSnippetSize(jetpackSearchSpec.getMaxSnippetSize());
+        if (jetpackSearchSpec.getResultGroupingTypeFlags() != 0) {
+            platformBuilder.setResultGrouping(
+                    jetpackSearchSpec.getResultGroupingTypeFlags(),
+                    jetpackSearchSpec.getResultGroupingLimit());
+        }
+        for (Map.Entry<String, List<String>> projection :
+                jetpackSearchSpec.getProjections().entrySet()) {
+            platformBuilder.addProjection(projection.getKey(), projection.getValue());
+        }
+        return platformBuilder.build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
new file mode 100644
index 0000000..ca52112
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.util.Preconditions;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link SetSchemaRequest}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class SetSchemaRequestToPlatformConverter {
+    private SetSchemaRequestToPlatformConverter() {}
+
+    /**
+     * Translates a jetpack {@link SetSchemaRequest} into a platform
+     * {@link android.app.appsearch.SetSchemaRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
+            @NonNull SetSchemaRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        android.app.appsearch.SetSchemaRequest.Builder platformBuilder =
+                new android.app.appsearch.SetSchemaRequest.Builder();
+        for (AppSearchSchema jetpackSchema : jetpackRequest.getSchemas()) {
+            platformBuilder.addSchemas(SchemaToPlatformConverter.toPlatformSchema(jetpackSchema));
+        }
+        for (String schemaNotDisplayedBySystem : jetpackRequest.getSchemasNotDisplayedBySystem()) {
+            platformBuilder.setSchemaTypeDisplayedBySystem(
+                    schemaNotDisplayedBySystem, /*displayed=*/ false);
+        }
+        for (Map.Entry<String, Set<PackageIdentifier>> jetpackSchemaVisibleToPackage :
+                jetpackRequest.getSchemasVisibleToPackagesInternal().entrySet()) {
+            for (PackageIdentifier jetpackPackageIdentifier :
+                    jetpackSchemaVisibleToPackage.getValue()) {
+                platformBuilder.setSchemaTypeVisibilityForPackage(
+                        jetpackSchemaVisibleToPackage.getKey(),
+                        /*visible=*/ true,
+                        new android.app.appsearch.PackageIdentifier(
+                                jetpackPackageIdentifier.getPackageName(),
+                                jetpackPackageIdentifier.getSha256Certificate()));
+            }
+        }
+        for (Map.Entry<String, Migrator> entry : jetpackRequest.getMigrators().entrySet()) {
+            Migrator jetpackMigrator = entry.getValue();
+            android.app.appsearch.Migrator platformMigrator = new android.app.appsearch.Migrator() {
+                @Override
+                public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                    return jetpackMigrator.shouldMigrate(currentVersion, finalVersion);
+                }
+
+                @NonNull
+                @Override
+                public android.app.appsearch.GenericDocument onUpgrade(
+                        int currentVersion,
+                        int finalVersion,
+                        @NonNull android.app.appsearch.GenericDocument inPlatformDocument) {
+                    GenericDocument inJetpackDocument =
+                            GenericDocumentToPlatformConverter.toJetpackGenericDocument(
+                                    inPlatformDocument);
+                    GenericDocument outJetpackDocument = jetpackMigrator.onUpgrade(
+                            currentVersion, finalVersion, inJetpackDocument);
+                    if (inJetpackDocument.equals(outJetpackDocument)) {
+                        return inPlatformDocument; // Same object; no conversion occurred.
+                    }
+                    return GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+                            outJetpackDocument);
+                }
+
+                @NonNull
+                @Override
+                public android.app.appsearch.GenericDocument onDowngrade(
+                        int currentVersion,
+                        int finalVersion,
+                        @NonNull android.app.appsearch.GenericDocument inPlatformDocument) {
+                    GenericDocument inJetpackDocument =
+                            GenericDocumentToPlatformConverter.toJetpackGenericDocument(
+                                    inPlatformDocument);
+                    GenericDocument outJetpackDocument = jetpackMigrator.onDowngrade(
+                            currentVersion, finalVersion, inJetpackDocument);
+                    if (inJetpackDocument.equals(outJetpackDocument)) {
+                        return inPlatformDocument; // Same object; no conversion occurred.
+                    }
+                    return GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+                            outJetpackDocument);
+                }
+            };
+            platformBuilder.setMigrator(entry.getKey(), platformMigrator);
+        }
+        return platformBuilder
+                .setForceOverride(jetpackRequest.isForceOverride())
+                .setVersion(jetpackRequest.getVersion())
+                .build();
+    }
+
+    /**
+     * Translates a platform {@link android.app.appsearch.SetSchemaResponse} into a jetpack
+     * {@link SetSchemaResponse}.
+     */
+    @NonNull
+    public static SetSchemaResponse toJetpackSetSchemaResponse(
+            @NonNull android.app.appsearch.SetSchemaResponse platformResponse) {
+        Preconditions.checkNotNull(platformResponse);
+        SetSchemaResponse.Builder jetpackBuilder = new SetSchemaResponse.Builder()
+                .addDeletedTypes(platformResponse.getDeletedTypes())
+                .addIncompatibleTypes(platformResponse.getIncompatibleTypes())
+                .addMigratedTypes(platformResponse.getMigratedTypes());
+        for (android.app.appsearch.SetSchemaResponse.MigrationFailure migrationFailure :
+                platformResponse.getMigrationFailures()) {
+            jetpackBuilder.addMigrationFailure(new SetSchemaResponse.MigrationFailure(
+                    migrationFailure.getNamespace(),
+                    migrationFailure.getDocumentId(),
+                    migrationFailure.getSchemaType(),
+                    AppSearchResultToPlatformConverter.platformAppSearchResultToJetpack(
+                            migrationFailure.getAppSearchResult())));
+        }
+        return jetpackBuilder.build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/util/BatchResultCallbackAdapter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/util/BatchResultCallbackAdapter.java
new file mode 100644
index 0000000..226e274
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/util/BatchResultCallbackAdapter.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.util;
+
+import android.app.appsearch.BatchResultCallback;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import java.util.function.Function;
+
+/**
+ * An implementation of the framework API's {@link android.app.appsearch.BatchResultCallback} which
+ * return the result as a {@link com.google.common.util.concurrent.ListenableFuture}.
+ *
+ * @param <K>             The type of key in the batch result (both Framework and Jetpack)
+ * @param <PlatformValue> The type of value in the Framework's
+ *                        {@link android.app.appsearch.AppSearchBatchResult}.
+ * @param <JetpackValue>  The type of value in Jetpack's {@link AppSearchBatchResult}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class BatchResultCallbackAdapter<K, PlatformValue, JetpackValue>
+        implements BatchResultCallback<K, PlatformValue> {
+    private final ResolvableFuture<AppSearchBatchResult<K, JetpackValue>> mFuture;
+    private final Function<PlatformValue, JetpackValue> mValueMapper;
+
+    public BatchResultCallbackAdapter(
+            @NonNull ResolvableFuture<AppSearchBatchResult<K, JetpackValue>> future,
+            @NonNull Function<PlatformValue, JetpackValue> valueMapper) {
+        mFuture = Preconditions.checkNotNull(future);
+        mValueMapper = Preconditions.checkNotNull(valueMapper);
+    }
+
+    @Override
+    public void onResult(
+            @NonNull android.app.appsearch.AppSearchBatchResult<K, PlatformValue> platformResult) {
+        AppSearchBatchResult<K, JetpackValue> jetpackResult =
+                AppSearchResultToPlatformConverter.platformAppSearchBatchResultToJetpack(
+                        platformResult, mValueMapper);
+        mFuture.set(jetpackResult);
+    }
+
+    @Override
+    public void onSystemError(@Nullable Throwable t) {
+        mFuture.setException(t);
+    }
+
+    /**
+     * Returns a {@link androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter} where
+     * the Platform value is identical to the Jetpack value, needing no transformation.
+     */
+    @NonNull
+    public static <K, V> BatchResultCallbackAdapter<K, V, V> forSameValueType(
+            @NonNull ResolvableFuture<AppSearchBatchResult<K, V>> future) {
+        return new BatchResultCallbackAdapter<>(future, Function.identity());
+    }
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 556ee60..9db151fd 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -25,7 +25,7 @@
     val ANNOTATION = Version("1.3.0-alpha01")
     val ANNOTATION_EXPERIMENTAL = Version("1.2.0-alpha01")
     val APPCOMPAT = Version("1.4.0-alpha03")
-    val APPSEARCH = Version("1.0.0-alpha01")
+    val APPSEARCH = Version("1.0.0-alpha03")
     val ARCH_CORE = Version("2.2.0-alpha01")
     val ARCH_CORE_TESTING = ARCH_CORE
     val ARCH_RUNTIME = Version("2.2.0-alpha01")
@@ -54,6 +54,7 @@
     val CORE_APPDIGEST = Version("1.0.0-alpha01")
     val CORE_GOOGLE_SHORTCUTS = Version("1.1.0-alpha01")
     val CORE_ROLE = Version("1.1.0-alpha02")
+    val CORE_SPLASHSCREEN = Version("1.0.0-alpha01")
     val CURSORADAPTER = Version("1.1.0-alpha01")
     val CUSTOMVIEW = Version("1.2.0-alpha01")
     val DATASTORE = Version("1.0.0-rc02")
@@ -152,5 +153,5 @@
     val WINDOW = Version("1.0.0-alpha09")
     val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
     val WINDOW_SIDECAR = Version("0.1.0-alpha01")
-    val WORK = Version("2.6.0-beta02")
+    val WORK = Version("2.7.0-alpha05")
 }
diff --git a/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt
index 95df021..3aa609f 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -39,7 +39,7 @@
      * Either an integer value or a pre-release platform code, prefixed with "android-" (ex.
      * "android-28" or "android-Q") as you would see within the SDK's platforms directory.
      */
-    const val COMPILE_SDK_VERSION = "android-30"
+    const val COMPILE_SDK_VERSION = "android-S"
 
     /**
      * The Android SDK version to use for targetSdkVersion meta-data.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index f4f27b5..10b99f0 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -42,6 +42,7 @@
  * This includes things like default template and session parameters, as well as maximum resolution
  * and aspect ratios for the display.
  */
+@Suppress("DEPRECATION")
 class CameraUseCaseAdapter(context: Context) : UseCaseConfigFactory {
 
     private val display: Display by lazy {
@@ -165,4 +166,4 @@
             // TODO: Add Camera2 options and callbacks
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index f2d49f8..39859f2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -188,6 +188,7 @@
 }
 
 @RequiresApi(Build.VERSION_CODES.P)
+@Suppress("DEPRECATION")
 internal object Api28Compat {
     @JvmStatic
     @Throws(CameraAccessException::class)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
index 4581562..89eee13 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
@@ -35,6 +35,7 @@
 
 @RunWith(Parameterized::class)
 @SmallTest
+@Suppress("DEPRECATION")
 public class Camera2CamcorderProfileProviderTest(private val quality: Int) {
     public companion object {
         @JvmStatic
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProvider.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProvider.java
index a1040d8..f88e1dc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProvider.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProvider.java
@@ -93,6 +93,7 @@
     }
 
     @Nullable
+    @SuppressWarnings("deprecation")
     private CamcorderProfileProxy getProfileInternal(int quality) {
         CamcorderProfile profile = null;
         try {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index e9ab04f..7f3dd92f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -69,6 +69,7 @@
             }
 
             @Override
+            @SuppressWarnings("deprecation")
             public CamcorderProfile get(int cameraId, int quality) {
                 return CamcorderProfile.get(cameraId, quality);
             }
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/CamcorderProfileProxyTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/CamcorderProfileProxyTest.kt
index 1f560eb..45b1947 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/CamcorderProfileProxyTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/CamcorderProfileProxyTest.kt
@@ -26,6 +26,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
+@Suppress("DEPRECATION")
 public class CamcorderProfileProxyTest {
 
     @Test
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
index 7548380..e875bae 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
@@ -1095,6 +1095,7 @@
     }
 
     /** Set audio record parameters by CamcorderProfile */
+    @SuppressWarnings("deprecation")
     private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
         CamcorderProfile profile;
         boolean isCamcorderProfileFound = false;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
index e5404ef..b917b5f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
@@ -146,6 +146,7 @@
 
     /** Crops byte array with given {@link android.graphics.Rect}. */
     @NonNull
+    @SuppressWarnings("deprecation")
     public static byte[] cropByteArray(@NonNull byte[] data, @Nullable Rect cropRect)
             throws CodecFailedException {
         if (cropRect == null) {
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
index 6afb829..b703fcb 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
@@ -68,6 +68,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
+@Suppress("DEPRECATION")
 class VideoEncoderTest {
 
     @get: Rule
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
index 7db9551..8360571 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
@@ -990,6 +990,7 @@
     }
 
     /** Set audio record parameters by CamcorderProfile */
+    @SuppressWarnings("deprecation")
     private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
         CamcorderProfile profile;
         boolean isCamcorderProfileFound = false;
diff --git a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt
index cd16141..186059f 100644
--- a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt
+++ b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt
@@ -41,6 +41,7 @@
  *
  * To use the viewfinder, call configure with the desired surface size, mode, and format.
  */
+@Suppress("DEPRECATION")
 class Viewfinder(
     context: Context?,
     attrs: AttributeSet?,
@@ -553,4 +554,4 @@
 
 internal fun Size.area(): Long {
     return this.width * this.height.toLong()
-}
\ No newline at end of file
+}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java
index 713443d..ef0331c 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java
@@ -51,6 +51,7 @@
         return new SurfaceWrapper(hostToken, width, height, displayId, densityDpi, surface);
     }
 
+    @SuppressWarnings("deprecation")
     private int densityDpi() {
         DisplayMetrics displayMetrics = new DisplayMetrics();
         mSurfaceView.getDisplay().getRealMetrics(displayMetrics);
diff --git a/car/app/app-samples/navigation/common/lint-baseline.xml b/car/app/app-samples/navigation/common/lint-baseline.xml
index 1f16ba2..e927ed7 100644
--- a/car/app/app-samples/navigation/common/lint-baseline.xml
+++ b/car/app/app-samples/navigation/common/lint-baseline.xml
@@ -96,7 +96,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/car/app/sample/navigation/common/nav/NavigationService.java"
-            line="502"
+            line="503"
             column="21"/>
     </issue>
 
@@ -107,7 +107,7 @@
         errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/car/app/sample/navigation/common/nav/NavigationService.java"
-            line="503"
+            line="504"
             column="34"/>
     </issue>
 
diff --git a/car/app/app-samples/showcase/common/lint-baseline.xml b/car/app/app-samples/showcase/common/lint-baseline.xml
index 34ce884..41d7627 100644
--- a/car/app/app-samples/showcase/common/lint-baseline.xml
+++ b/car/app/app-samples/showcase/common/lint-baseline.xml
@@ -3,6 +3,17 @@
 
     <issue
         id="ClassVerificationFailure"
+        message="This call references a method added in API level 26; however, the containing class androidx.car.app.sample.showcase.common.navigation.NavigationNotificationService is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                    new NotificationChannel("
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/car/app/sample/showcase/common/navigation/NavigationNotificationService.java"
+            line="112"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
         message="This call references a method added in API level 26; however, the containing class androidx.car.app.sample.showcase.common.navigation.NavigationNotificationsDemoScreen is reachable from earlier API levels and will fail run-time class verification."
         errorLine1="                                        context.startForegroundService(intent);"
         errorLine2="                                                ~~~~~~~~~~~~~~~~~~~~~~">
@@ -12,4 +23,15 @@
             column="49"/>
     </issue>
 
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 26; however, the containing class androidx.car.app.sample.showcase.common.misc.NotificationDemoScreen is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                    new NotificationChannel("
+        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/car/app/sample/showcase/common/misc/NotificationDemoScreen.java"
+            line="228"
+            column="21"/>
+    </issue>
+
 </issues>
diff --git a/core/core-appdigest/build.gradle b/core/core-appdigest/build.gradle
index c9f7302..12a4a92 100644
--- a/core/core-appdigest/build.gradle
+++ b/core/core-appdigest/build.gradle
@@ -25,7 +25,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.0.0")
-    api("androidx.core:core:1.0.0")
+    implementation(project(":core:core"))
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
diff --git a/core/core-appdigest/lint-baseline.xml b/core/core-appdigest/lint-baseline.xml
index f787484e..425cdb0 100644
--- a/core/core-appdigest/lint-baseline.xml
+++ b/core/core-appdigest/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-alpha02" type="baseline" client="cli" name="Lint" variant="all" version="7.1.0-alpha02">
+<issues format="6" by="lint 7.1.0-alpha03" type="baseline" client="gradle" name="AGP (7.1.0-alpha03)" variant="all" version="7.1.0-alpha03">
 
     <issue
         id="NewApi"
@@ -8,7 +8,7 @@
         errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java"
-            line="379"
+            line="621"
             column="31"/>
     </issue>
 
@@ -19,8 +19,216 @@
         errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java"
-            line="395"
+            line="637"
             column="35"/>
     </issue>
 
+    <issue
+        id="WrongConstant"
+        message="Must be one or more of: Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, Checksum.TYPE_WHOLE_MD5, Checksum.TYPE_WHOLE_SHA1, Checksum.TYPE_WHOLE_SHA256, Checksum.TYPE_WHOLE_SHA512, Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512"
+        errorLine1="                                            apkChecksum.getType(), apkChecksum.getValue(),"
+        errorLine2="                                            ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="204"
+            column="45"/>
+    </issue>
+        <issue
+        id="NewApi"
+        message="Field requires API level S (current min is 14): `android.content.pm.PackageManager#TRUST_ALL`"
+        errorLine1="                trustedInstallers = PackageManager.TRUST_ALL;"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="180"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level S (current min is 14): `android.content.pm.PackageManager#TRUST_NONE`"
+        errorLine1="                trustedInstallers = PackageManager.TRUST_NONE;"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="182"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level S (current min is 14): `android.content.pm.PackageManager#requestChecksums`"
+        errorLine1="            context.getPackageManager().requestChecksums(packageName, includeSplits, required,"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="189"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Cast to `OnChecksumsReadyListener` requires API level 31 (current min is 14)"
+        errorLine1="                    trustedInstallers, new PackageManager.OnChecksumsReadyListener() {"
+        errorLine2="                                       ^">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="190"
+            column="40"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level S (current min is 14): `android.content.pm.PackageManager.OnChecksumsReadyListener`"
+        errorLine1="                    trustedInstallers, new PackageManager.OnChecksumsReadyListener() {"
+        errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="190"
+            column="44"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level S (current min is 14): `android.content.pm.ApkChecksum#getSplitName`"
+        errorLine1="                                    checksums[i] = new Checksum(apkChecksum.getSplitName(),"
+        errorLine2="                                                                            ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="203"
+            column="77"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level S (current min is 14): `android.content.pm.ApkChecksum#getType`"
+        errorLine1="                                            apkChecksum.getType(), apkChecksum.getValue(),"
+        errorLine2="                                                        ~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="204"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level S (current min is 14): `android.content.pm.ApkChecksum#getValue`"
+        errorLine1="                                            apkChecksum.getType(), apkChecksum.getValue(),"
+        errorLine2="                                                                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="204"
+            column="80"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level S (current min is 14): `android.content.pm.ApkChecksum#getInstallerPackageName`"
+        errorLine1="                                            apkChecksum.getInstallerPackageName(),"
+        errorLine2="                                                        ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="205"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level S (current min is 14): `android.content.pm.ApkChecksum#getInstallerCertificate`"
+        errorLine1="                                            apkChecksum.getInstallerCertificate());"
+        errorLine2="                                                        ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="206"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 24): `android.app.UiAutomation#adoptShellPermissionIdentity`"
+        errorLine1="            getUiAutomation().adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES);"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java"
+            line="621"
+            column="31"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 29 (current min is 24): `android.app.UiAutomation#dropShellPermissionIdentity`"
+        errorLine1="                getUiAutomation().dropShellPermissionIdentity();"
+        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java"
+            line="637"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 31; however, the containing class androidx.core.appdigest.Checksums.ApiSImpl is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            context.getPackageManager().requestChecksums(packageName, includeSplits, required,"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="189"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 31; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                                    checksums[i] = new Checksum(apkChecksum.getSplitName(),"
+        errorLine2="                                                                            ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="203"
+            column="77"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 31; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                                            apkChecksum.getType(), apkChecksum.getValue(),"
+        errorLine2="                                                        ~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="204"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 31; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                                            apkChecksum.getType(), apkChecksum.getValue(),"
+        errorLine2="                                                                               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="204"
+            column="80"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 31; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                                            apkChecksum.getInstallerPackageName(),"
+        errorLine2="                                                        ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="205"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 31; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                                            apkChecksum.getInstallerCertificate());"
+        errorLine2="                                                        ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/appdigest/Checksums.java"
+            line="206"
+            column="57"/>
+    </issue>
+
 </issues>
diff --git a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
index 6d09208..16eff3c 100644
--- a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
+++ b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
@@ -27,6 +27,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.Manifest;
@@ -43,6 +44,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -90,6 +92,8 @@
     private static final String TEST_FIXED_APK_VERITY = "CtsPkgInstallTinyAppV2V3V4-Verity.apk";
 
     private static final String TEST_FIXED_APK_MD5 = "c19868da017dc01467169f8ea7c5bc57";
+    private static final String TEST_FIXED_APK_V2_SHA256 =
+            "1eec9e86e322b8d7e48e255fc3f2df2dbc91036e63982ff9850597c6a37bbeb3";
     private static final String TEST_FIXED_APK_SHA256 =
             "91aa30c1ce8d0474052f71cb8210691d41f534989c5521e27e794ec4f754c5ef";
     private static final String TEST_FIXED_APK_SHA512 =
@@ -100,7 +104,8 @@
             TYPE_WHOLE_MERKLE_ROOT_4K_SHA256 | TYPE_WHOLE_MD5 | TYPE_WHOLE_SHA1 | TYPE_WHOLE_SHA256
                     | TYPE_WHOLE_SHA512
                     | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256 | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512;
-
+    private static final char[] HEX_LOWER_CASE_DIGITS =
+            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
     private Context mContext;
     private Executor mExecutor;
 
@@ -109,225 +114,6 @@
         uninstallPackageSilently(FIXED_PACKAGE_NAME);
     }
 
-    @Before
-    public void onBefore() throws Exception {
-        mContext = ApplicationProvider.getApplicationContext();
-        mExecutor = Executors.newCachedThreadPool();
-    }
-
-    @After
-    public void onAfter() throws Exception {
-        uninstallPackageSilently(V4_PACKAGE_NAME);
-        assertFalse(isAppInstalled(V4_PACKAGE_NAME));
-        uninstallPackageSilently(FIXED_PACKAGE_NAME);
-        assertFalse(isAppInstalled(FIXED_PACKAGE_NAME));
-    }
-
-    @SmallTest
-    @Test
-    public void testDefaultChecksums() throws Exception {
-        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, 0, Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testSplitsSha256() throws Exception {
-        installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
-                TEST_V4_SPLIT3, TEST_V4_SPLIT4});
-        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, TYPE_WHOLE_SHA256,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(6, checksums.length);
-        assertEquals(checksums[0].getSplitName(), null);
-        assertEquals(checksums[0].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[0].getValue()),
-                "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
-        assertEquals(checksums[1].getSplitName(), "config.hdpi");
-        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[1].getValue()),
-                "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
-        assertEquals(checksums[2].getSplitName(), "config.mdpi");
-        assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[2].getValue()),
-                "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
-        assertEquals(checksums[3].getSplitName(), "config.xhdpi");
-        assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[3].getValue()),
-                "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
-        assertEquals(checksums[4].getSplitName(), "config.xxhdpi");
-        assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[4].getValue()),
-                "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
-        assertEquals(checksums[5].getSplitName(), "config.xxxhdpi");
-        assertEquals(checksums[5].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[5].getValue()),
-                "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedDefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedV1DefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_V1);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedSha512DefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_V2_SHA512);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedVerityDefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_VERITY);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        // No usable hashes as verity-in-v2-signature does not cover the whole file.
-        assertEquals(0, checksums.length);
-    }
-
-    @LargeTest
-    @Test
-    public void testAllChecksums() throws Exception {
-        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, ALL_CHECKSUMS,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(5, checksums.length);
-        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
-        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
-        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
-        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
-        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedAllChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
-                Checksums.TRUST_NONE);
-        validateFixedAllChecksums(checksums);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedAllChecksumsDirectExecutor() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(mContext, new Executor() {
-                    @Override
-                    public void execute(Runnable command) {
-                        command.run();
-                    }
-                }, FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
-        validateFixedAllChecksums(checksums);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedAllChecksumsSingleThread() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(mContext, Executors.newSingleThreadExecutor(),
-                FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
-        validateFixedAllChecksums(checksums);
-    }
-
-    private void validateFixedAllChecksums(Checksum[] checksums) {
-        assertNotNull(checksums);
-        assertEquals(5, checksums.length);
-        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
-        assertEquals("90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a",
-                bytesToHexString(checksums[0].getValue()));
-        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
-        assertEquals(TEST_FIXED_APK_MD5, bytesToHexString(checksums[1].getValue()));
-        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
-        assertEquals("331eef6bc57671de28cbd7e32089d047285ade6a",
-                bytesToHexString(checksums[2].getValue()));
-        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
-        assertEquals(TEST_FIXED_APK_SHA256, bytesToHexString(checksums[3].getValue()));
-        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
-        assertEquals(TEST_FIXED_APK_SHA512, bytesToHexString(checksums[4].getValue()));
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedV1AllChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_V1);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(5, checksums.length);
-        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
-        assertEquals("1e8f831ef35257ca30d11668520aaafc6da243e853531caabc3b7867986f8886",
-                bytesToHexString(checksums[0].getValue()));
-        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
-        assertEquals(bytesToHexString(checksums[1].getValue()), "78e51e8c51e4adc6870cd71389e0f3db");
-        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
-        assertEquals("f6654505f2274fd9bfc098b660cdfdc2e4da6d53",
-                bytesToHexString(checksums[2].getValue()));
-        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
-        assertEquals("43755d36ec944494f6275ee92662aca95079b3aa6639f2d35208c5af15adff78",
-                bytesToHexString(checksums[3].getValue()));
-        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
-        assertEquals("030fc815a4957c163af2bc6f30dd5b48ac09c94c25a824a514609e1476f91421"
-                        + "e2c8b6baa16ef54014ad6c5b90c37b26b0f5c8aeb01b63a1db2eca133091c8d1",
-                bytesToHexString(checksums[4].getValue()));
-    }
-
-    private Checksum[] getChecksums(@NonNull String packageName, boolean includeSplits,
-            @Checksum.Type int required, @NonNull List<Certificate> trustedInstallers)
-            throws Exception {
-        return getChecksums(mContext, mExecutor, packageName, includeSplits, required,
-                trustedInstallers);
-    }
-
     private static Checksum[] getChecksums(@NonNull Context context, @NonNull Executor executor,
             @NonNull String packageName,
             boolean includeSplits,
@@ -357,111 +143,6 @@
         return checksums;
     }
 
-    private void installPackage(String baseName) throws Exception {
-        installSplits(new String[]{baseName});
-    }
-
-    void installSplits(String[] names) throws Exception {
-        if (Build.VERSION.SDK_INT >= 24) {
-            new InstallerApi24(mContext).installSplits(names);
-        }
-    }
-
-    @RequiresApi(24)
-    static class InstallerApi24 {
-        private Context mContext;
-
-        InstallerApi24(Context context) {
-            mContext = context;
-        }
-
-        void installSplits(String[] names) throws Exception {
-            getUiAutomation().adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES);
-            try {
-                final PackageInstaller installer =
-                        mContext.getPackageManager().getPackageInstaller();
-                final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
-                        PackageInstaller.SessionParams.MODE_FULL_INSTALL);
-
-                final int sessionId = installer.createSession(params);
-                PackageInstaller.Session session = installer.openSession(sessionId);
-
-                for (String name : names) {
-                    writeFileToSession(session, name, name);
-                }
-
-                commitSession(session);
-            } finally {
-                getUiAutomation().dropShellPermissionIdentity();
-            }
-        }
-
-        private static void writeFileToSession(PackageInstaller.Session session, String name,
-                String apk) throws IOException {
-            try (OutputStream os = session.openWrite(name, 0, -1);
-                 InputStream is = getResourceAsStream(apk)) {
-                assertNotNull(name, is);
-                writeFullStream(is, os, -1);
-            }
-        }
-
-        private void commitSession(PackageInstaller.Session session) throws Exception {
-            final ResolvableFuture<Intent> result = ResolvableFuture.create();
-
-            // Create a single-use broadcast receiver
-            BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    context.unregisterReceiver(this);
-                    result.set(intent);
-                }
-            };
-
-            // Create a matching intent-filter and register the receiver
-            final int resultId = result.hashCode();
-            final String action = "androidx.core.appdigest.COMMIT_COMPLETE." + resultId;
-            IntentFilter intentFilter = new IntentFilter();
-            intentFilter.addAction(action);
-            mContext.registerReceiver(broadcastReceiver, intentFilter);
-
-            Intent intent = new Intent(action);
-            PendingIntent sender = PendingIntent.getBroadcast(mContext, resultId, intent,
-                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
-            session.commit(sender.getIntentSender());
-
-            Intent commitResult = result.get();
-            final int status = commitResult.getIntExtra(PackageInstaller.EXTRA_STATUS,
-                    PackageInstaller.STATUS_FAILURE);
-            assertEquals(commitResult.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + " OR "
-                            + commitResult.getExtras().get(Intent.EXTRA_INTENT),
-                    PackageInstaller.STATUS_SUCCESS, status);
-        }
-
-        static UiAutomation getUiAutomation() {
-            return InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        }
-
-        static String executeShellCommand(String command) throws IOException {
-            final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
-            try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
-                return readFullStream(inputStream);
-            }
-        }
-
-        static boolean isAppInstalled(final String packageName) throws IOException {
-            final String commandResult = executeShellCommand("pm list packages");
-            final int prefixLength = "package:".length();
-            return Arrays.stream(commandResult.split("\\r?\\n"))
-                    .anyMatch(new Predicate<String>() {
-                        @Override
-                        public boolean test(String line) {
-                            return line.substring(prefixLength).equals(packageName);
-                        }
-                    });
-        }
-    }
-
     public static InputStream getResourceAsStream(String name) {
         return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
     }
@@ -498,16 +179,6 @@
         executeShellCommand("pm uninstall " + packageName);
     }
 
-    private boolean isAppInstalled(String packageName) throws IOException {
-        if (Build.VERSION.SDK_INT >= 24) {
-            return InstallerApi24.isAppInstalled(packageName);
-        }
-        return false;
-    }
-
-    private static final char[] HEX_LOWER_CASE_DIGITS =
-            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
-
     @NonNull
     private static String bytesToHexString(byte[] array) {
         int offset = 0;
@@ -536,4 +207,468 @@
         }
         return data;
     }
+
+    @Before
+    public void onBefore() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mExecutor = Executors.newCachedThreadPool();
+    }
+
+    @After
+    public void onAfter() throws Exception {
+        uninstallPackageSilently(V4_PACKAGE_NAME);
+        assertFalse(isAppInstalled(V4_PACKAGE_NAME));
+        uninstallPackageSilently(FIXED_PACKAGE_NAME);
+        assertFalse(isAppInstalled(FIXED_PACKAGE_NAME));
+    }
+
+    @SmallTest
+    @Test
+    public void testDefaultChecksums() throws Exception {
+        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, 0, Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(1, checksums.length);
+            assertEquals(checksums[0].getType(),
+                    android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testSplitsDefaultChecksums() throws Exception {
+        installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
+                TEST_V4_SPLIT3, TEST_V4_SPLIT4});
+        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, 0, Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(checksums.length, 6);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(null, checksums[0].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[0].getType());
+            assertEquals("config.hdpi", checksums[1].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[1].getType());
+            assertEquals("config.mdpi", checksums[2].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[2].getType());
+            assertEquals("config.xhdpi", checksums[3].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[3].getType());
+            assertEquals("config.xxhdpi", checksums[4].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[4].getType());
+            assertEquals("config.xxxhdpi", checksums[5].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testSplitsSha256() throws Exception {
+        installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
+                TEST_V4_SPLIT3, TEST_V4_SPLIT4});
+        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, TYPE_WHOLE_SHA256,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(checksums.length, 12);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(null, checksums[0].getSplitName());
+            assertEquals(TYPE_WHOLE_SHA256, checksums[0].getType());
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
+            assertEquals(null, checksums[1].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[1].getType());
+            assertEquals(checksums[2].getSplitName(), "config.hdpi");
+            assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
+            assertEquals("config.hdpi", checksums[3].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[3].getType());
+            assertEquals(checksums[4].getSplitName(), "config.mdpi");
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
+            assertEquals("config.mdpi", checksums[5].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+            assertEquals(checksums[6].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[6].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[6].getValue()),
+                    "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
+            assertEquals("config.xhdpi", checksums[7].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[7].getType());
+            assertEquals(checksums[8].getSplitName(), "config.xxhdpi");
+            assertEquals(checksums[8].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[8].getValue()),
+                    "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
+            assertEquals("config.xxhdpi", checksums[9].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[9].getType());
+            assertEquals(checksums[10].getSplitName(), "config.xxxhdpi");
+            assertEquals(checksums[10].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[10].getValue()),
+                    "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
+            assertEquals("config.xxxhdpi", checksums[11].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[11].getType());
+        } else {
+            assertEquals(6, checksums.length);
+            assertEquals(checksums[0].getSplitName(), null);
+            assertEquals(checksums[0].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
+            assertEquals(checksums[1].getSplitName(), "config.hdpi");
+            assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[1].getValue()),
+                    "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
+            assertEquals(checksums[2].getSplitName(), "config.mdpi");
+            assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
+            assertEquals(checksums[3].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[3].getValue()),
+                    "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
+            assertEquals(checksums[4].getSplitName(), "config.xxhdpi");
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
+            assertEquals(checksums[5].getSplitName(), "config.xxxhdpi");
+            assertEquals(checksums[5].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[5].getValue()),
+                    "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedDefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(1, checksums.length);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[0].getType());
+            assertEquals(TEST_FIXED_APK_V2_SHA256, bytesToHexString(checksums[0].getValue()));
+            assertNull(checksums[0].getInstallerCertificate());
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedV1DefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V1);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        assertEquals(0, checksums.length);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedSha512DefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V2_SHA512);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(1, checksums.length);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, checksums[0].getType());
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "6b866e8a54a3e358dfc20007960fb96123845f6c6d6c45f5fddf88150d71677f"
+                            + "4c3081a58921c88651f7376118aca312cf764b391cdfb8a18c6710f9f27916a0");
+            assertNull(checksums[0].getInstallerCertificate());
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedVerityDefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_VERITY);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        // No usable hashes as verity-in-v2-signature does not cover the whole file.
+        assertEquals(0, checksums.length);
+    }
+
+    @LargeTest
+    @Test
+    public void testAllChecksums() throws Exception {
+        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, ALL_CHECKSUMS,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(checksums.length, 7);
+            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+            assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+            assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+            assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+            assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, checksums[6].getType());
+        } else {
+            assertEquals(5, checksums.length);
+            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+            assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+            assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+            assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+            assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedAllChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
+                Checksums.TRUST_NONE);
+        validateFixedAllChecksums(checksums);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedAllChecksumsDirectExecutor() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(mContext, new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        }, FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
+        validateFixedAllChecksums(checksums);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedAllChecksumsSingleThread() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(mContext, Executors.newSingleThreadExecutor(),
+                FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
+        validateFixedAllChecksums(checksums);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedV1AllChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V1);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        assertEquals(5, checksums.length);
+        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+        assertEquals("1e8f831ef35257ca30d11668520aaafc6da243e853531caabc3b7867986f8886",
+                bytesToHexString(checksums[0].getValue()));
+        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+        assertEquals(bytesToHexString(checksums[1].getValue()), "78e51e8c51e4adc6870cd71389e0f3db");
+        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+        assertEquals("f6654505f2274fd9bfc098b660cdfdc2e4da6d53",
+                bytesToHexString(checksums[2].getValue()));
+        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+        assertEquals("43755d36ec944494f6275ee92662aca95079b3aa6639f2d35208c5af15adff78",
+                bytesToHexString(checksums[3].getValue()));
+        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+        assertEquals("030fc815a4957c163af2bc6f30dd5b48ac09c94c25a824a514609e1476f91421"
+                        + "e2c8b6baa16ef54014ad6c5b90c37b26b0f5c8aeb01b63a1db2eca133091c8d1",
+                bytesToHexString(checksums[4].getValue()));
+    }
+
+    private void validateFixedAllChecksums(Checksum[] checksums) {
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastR()) {
+            assertEquals(checksums.length, 7);
+            assertEquals(checksums[0].getType(),
+                    android.content.pm.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a");
+            assertEquals(checksums[1].getType(), android.content.pm.Checksum.TYPE_WHOLE_MD5);
+            assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_MD5);
+            assertEquals(checksums[2].getType(), android.content.pm.Checksum.TYPE_WHOLE_SHA1);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "331eef6bc57671de28cbd7e32089d047285ade6a");
+            assertEquals(checksums[3].getType(), android.content.pm.Checksum.TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[3].getValue()), TEST_FIXED_APK_SHA256);
+            assertEquals(checksums[4].getType(), android.content.pm.Checksum.TYPE_WHOLE_SHA512);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "b59467fe578ebc81974ab3aaa1e0d2a76fef3e4ea7212a6f2885cec1af5253571"
+                            + "1e2e94496224cae3eba8dc992144ade321540ebd458ec5b9e6a4cc51170e018");
+            assertEquals(checksums[5].getType(),
+                    android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertEquals(bytesToHexString(checksums[5].getValue()), TEST_FIXED_APK_V2_SHA256);
+            assertEquals(checksums[6].getType(),
+                    android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512);
+            assertEquals(bytesToHexString(checksums[6].getValue()),
+                    "ef80a8630283f60108e8557c924307d0ccdfb6bbbf2c0176bd49af342f43bc84"
+                            + "5f2888afcb71524196dda0d6dd16a6a3292bb75b431b8ff74fb60d796e882f80");
+        } else {
+            assertEquals(5, checksums.length);
+            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+            assertEquals("90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a",
+                    bytesToHexString(checksums[0].getValue()));
+            assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+            assertEquals(TEST_FIXED_APK_MD5, bytesToHexString(checksums[1].getValue()));
+            assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+            assertEquals("331eef6bc57671de28cbd7e32089d047285ade6a",
+                    bytesToHexString(checksums[2].getValue()));
+            assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+            assertEquals(TEST_FIXED_APK_SHA256, bytesToHexString(checksums[3].getValue()));
+            assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+            assertEquals(TEST_FIXED_APK_SHA512, bytesToHexString(checksums[4].getValue()));
+        }
+    }
+
+    private Checksum[] getChecksums(@NonNull String packageName, boolean includeSplits,
+            @Checksum.Type int required, @NonNull List<Certificate> trustedInstallers)
+            throws Exception {
+        return getChecksums(mContext, mExecutor, packageName, includeSplits, required,
+                trustedInstallers);
+    }
+
+    private void installPackage(String baseName) throws Exception {
+        installSplits(new String[]{baseName});
+    }
+
+    void installSplits(String[] names) throws Exception {
+        if (Build.VERSION.SDK_INT >= 24) {
+            new InstallerApi24(mContext).installSplits(names);
+        }
+    }
+
+    private boolean isAppInstalled(String packageName) throws IOException {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return InstallerApi24.isAppInstalled(packageName);
+        }
+        return false;
+    }
+
+    @RequiresApi(24)
+    static class InstallerApi24 {
+        private Context mContext;
+
+        InstallerApi24(Context context) {
+            mContext = context;
+        }
+
+        private static void writeFileToSession(PackageInstaller.Session session, String name,
+                String apk) throws IOException {
+            try (OutputStream os = session.openWrite(name, 0, -1);
+                 InputStream is = getResourceAsStream(apk)) {
+                assertNotNull(name, is);
+                writeFullStream(is, os, -1);
+            }
+        }
+
+        static UiAutomation getUiAutomation() {
+            return InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        }
+
+        static String executeShellCommand(String command) throws IOException {
+            final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
+            try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
+                return readFullStream(inputStream);
+            }
+        }
+
+        static boolean isAppInstalled(final String packageName) throws IOException {
+            final String commandResult = executeShellCommand("pm list packages");
+            final int prefixLength = "package:".length();
+            return Arrays.stream(commandResult.split("\\r?\\n"))
+                    .anyMatch(new Predicate<String>() {
+                        @Override
+                        public boolean test(String line) {
+                            return line.substring(prefixLength).equals(packageName);
+                        }
+                    });
+        }
+
+        void installSplits(String[] names) throws Exception {
+            getUiAutomation().adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES);
+            try {
+                final PackageInstaller installer =
+                        mContext.getPackageManager().getPackageInstaller();
+                final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+                        PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+
+                final int sessionId = installer.createSession(params);
+                PackageInstaller.Session session = installer.openSession(sessionId);
+
+                for (String name : names) {
+                    writeFileToSession(session, name, name);
+                }
+
+                commitSession(session);
+            } finally {
+                getUiAutomation().dropShellPermissionIdentity();
+            }
+        }
+
+        private void commitSession(PackageInstaller.Session session) throws Exception {
+            final ResolvableFuture<Intent> result = ResolvableFuture.create();
+
+            // Create a single-use broadcast receiver
+            BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    context.unregisterReceiver(this);
+                    result.set(intent);
+                }
+            };
+
+            // Create a matching intent-filter and register the receiver
+            final int resultId = result.hashCode();
+            final String action = "androidx.core.appdigest.COMMIT_COMPLETE." + resultId;
+            IntentFilter intentFilter = new IntentFilter();
+            intentFilter.addAction(action);
+            mContext.registerReceiver(broadcastReceiver, intentFilter);
+
+            Intent intent = new Intent(action);
+            PendingIntent sender = PendingIntent.getBroadcast(mContext, resultId, intent,
+                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+
+            session.commit(sender.getIntentSender());
+
+            Intent commitResult = result.get();
+            final int status = commitResult.getIntExtra(PackageInstaller.EXTRA_STATUS,
+                    PackageInstaller.STATUS_FAILURE);
+            assertEquals(commitResult.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + " OR "
+                            + commitResult.getExtras().get(Intent.EXTRA_INTENT),
+                    PackageInstaller.STATUS_SUCCESS, status);
+        }
+    }
 }
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
index 2046117..ff05b32 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
@@ -23,6 +23,7 @@
 import static androidx.core.appdigest.Checksum.TYPE_WHOLE_SHA512;
 
 import android.content.Context;
+import android.content.pm.ApkChecksum;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Build;
@@ -30,8 +31,10 @@
 import android.util.Pair;
 import android.util.SparseArray;
 
+import androidx.annotation.ChecksSdkIntAtLeast;
 import androidx.annotation.NonNull;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -115,6 +118,11 @@
         Preconditions.checkNotNull(trustedInstallers);
         Preconditions.checkNotNull(executor);
 
+        if (BuildCompat.isAtLeastS()) {
+            return ApiSImpl.getChecksums(context, packageName, includeSplits, required,
+                    trustedInstallers, executor);
+        }
+
         final ApplicationInfo applicationInfo =
                 context.getPackageManager().getApplicationInfo(packageName, 0);
         if (applicationInfo == null) {
@@ -158,6 +166,56 @@
         return result;
     }
 
+    private static class ApiSImpl {
+        private ApiSImpl() {}
+
+        @ChecksSdkIntAtLeast(codename = "S") static
+        @NonNull ListenableFuture<Checksum[]> getChecksums(@NonNull Context context,
+                @NonNull String packageName, boolean includeSplits, @Checksum.Type int required,
+                @NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)
+                throws CertificateEncodingException, PackageManager.NameNotFoundException {
+            final ResolvableFuture<Checksum[]> result = ResolvableFuture.create();
+
+            if (trustedInstallers == TRUST_ALL) {
+                trustedInstallers = PackageManager.TRUST_ALL;
+            } else if (trustedInstallers == TRUST_NONE) {
+                trustedInstallers = PackageManager.TRUST_NONE;
+            } else if (trustedInstallers.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "trustedInstallers has to be one of TRUST_ALL/TRUST_NONE or a non-empty "
+                                + "list of certificates.");
+            }
+
+            context.getPackageManager().requestChecksums(packageName, includeSplits, required,
+                    trustedInstallers, new PackageManager.OnChecksumsReadyListener() {
+                        @Override
+                        public void onChecksumsReady(List<ApkChecksum> apkChecksums) {
+                            if (apkChecksums == null) {
+                                result.setException(
+                                        new IllegalStateException("Checksums missing."));
+                                return;
+                            }
+
+                            try {
+                                Checksum[] checksums = new Checksum[apkChecksums.size()];
+                                for (int i = 0, size = apkChecksums.size(); i < size; ++i) {
+                                    ApkChecksum apkChecksum = apkChecksums.get(i);
+                                    checksums[i] = new Checksum(apkChecksum.getSplitName(),
+                                            apkChecksum.getType(), apkChecksum.getValue(),
+                                            apkChecksum.getInstallerPackageName(),
+                                            apkChecksum.getInstallerCertificate());
+                                }
+                                result.set(checksums);
+                            } catch (Throwable e) {
+                                result.setException(e);
+                            }
+                        }
+                    });
+
+            return result;
+        }
+    }
+
     private static void getChecksumsSync(@NonNull List<Pair<String, File>> filesToChecksum,
             @Checksum.Type int required, ResolvableFuture<Checksum[]> result) {
         List<Checksum> allChecksums = new ArrayList<>();
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index 2608196..e27488b 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.LibraryType
diff --git a/core/core-ktx/lint-baseline.xml b/core/core-ktx/lint-baseline.xml
index 0a1170e..5f61a85 100644
--- a/core/core-ktx/lint-baseline.xml
+++ b/core/core-ktx/lint-baseline.xml
@@ -47,6 +47,17 @@
 
     <issue
         id="NewApi"
+        message="Call requires API level S (current min is 14): `android.util.SparseArray#set`"
+        errorLine1="        array[1] = &quot;one&quot;"
+        errorLine2="        ~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/core/util/SparseArrayTest.kt"
+            line="61"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="NewApi"
         message="Call requires API level 17 (current min is 14): `updatePaddingRelative`"
         errorLine1="        view.updatePaddingRelative(start = 10, end = 20)"
         errorLine2="             ~~~~~~~~~~~~~~~~~~~~~">
diff --git a/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt b/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt
index f2f373e..5d922f5 100644
--- a/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt
+++ b/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+@file:Suppress("NOTHING_TO_INLINE", "EXTENSION_SHADOWED_BY_MEMBER")
 
 package androidx.core.util
 
diff --git a/core/core-remoteviews/api/current.txt b/core/core-remoteviews/api/current.txt
new file mode 100644
index 0000000..6d78713
--- /dev/null
+++ b/core/core-remoteviews/api/current.txt
@@ -0,0 +1,28 @@
+// Signature format: 4.0
+package androidx.core.widget {
+
+  public final class RemoteViewsCompat {
+    method public static void setRemoteAdapter(android.content.Context context, android.widget.RemoteViews remoteViews, int appWidgetId, @IdRes int viewId, androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems items);
+    field public static final androidx.core.widget.RemoteViewsCompat INSTANCE;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems {
+    method public int getItemCount();
+    method public long getItemId(int position);
+    method public android.widget.RemoteViews getItemView(int position);
+    method public int getViewTypeCount();
+    method public boolean hasStableIds();
+    property public final int itemCount;
+    property public final int viewTypeCount;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems.Builder {
+    ctor public RemoteViewsCompat.RemoteCollectionItems.Builder();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder addItem(long id, android.widget.RemoteViews view);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems build();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setHasStableIds(boolean hasStableIds);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setViewTypeCount(int viewTypeCount);
+  }
+
+}
+
diff --git a/core/core-remoteviews/api/public_plus_experimental_current.txt b/core/core-remoteviews/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..6d78713
--- /dev/null
+++ b/core/core-remoteviews/api/public_plus_experimental_current.txt
@@ -0,0 +1,28 @@
+// Signature format: 4.0
+package androidx.core.widget {
+
+  public final class RemoteViewsCompat {
+    method public static void setRemoteAdapter(android.content.Context context, android.widget.RemoteViews remoteViews, int appWidgetId, @IdRes int viewId, androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems items);
+    field public static final androidx.core.widget.RemoteViewsCompat INSTANCE;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems {
+    method public int getItemCount();
+    method public long getItemId(int position);
+    method public android.widget.RemoteViews getItemView(int position);
+    method public int getViewTypeCount();
+    method public boolean hasStableIds();
+    property public final int itemCount;
+    property public final int viewTypeCount;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems.Builder {
+    ctor public RemoteViewsCompat.RemoteCollectionItems.Builder();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder addItem(long id, android.widget.RemoteViews view);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems build();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setHasStableIds(boolean hasStableIds);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setViewTypeCount(int viewTypeCount);
+  }
+
+}
+
diff --git a/work/workmanager-gcm/api/res-2.6.0-beta01.txt b/core/core-remoteviews/api/res-current.txt
similarity index 100%
copy from work/workmanager-gcm/api/res-2.6.0-beta01.txt
copy to core/core-remoteviews/api/res-current.txt
diff --git a/core/core-remoteviews/api/restricted_current.txt b/core/core-remoteviews/api/restricted_current.txt
new file mode 100644
index 0000000..a081062
--- /dev/null
+++ b/core/core-remoteviews/api/restricted_current.txt
@@ -0,0 +1,32 @@
+// Signature format: 4.0
+package androidx.core.widget {
+
+  public final class RemoteViewsCompat {
+    method public static void setRemoteAdapter(android.content.Context context, android.widget.RemoteViews remoteViews, int appWidgetId, @IdRes int viewId, androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems items);
+    field public static final androidx.core.widget.RemoteViewsCompat INSTANCE;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems {
+    method public int getItemCount();
+    method public long getItemId(int position);
+    method public android.widget.RemoteViews getItemView(int position);
+    method public int getViewTypeCount();
+    method public boolean hasStableIds();
+    property public final int itemCount;
+    property public final int viewTypeCount;
+  }
+
+  public static final class RemoteViewsCompat.RemoteCollectionItems.Builder {
+    ctor public RemoteViewsCompat.RemoteCollectionItems.Builder();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder addItem(long id, android.widget.RemoteViews view);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems build();
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setHasStableIds(boolean hasStableIds);
+    method public androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems.Builder setViewTypeCount(int viewTypeCount);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class RemoteViewsCompatService extends android.widget.RemoteViewsService {
+    method public android.widget.RemoteViewsService.RemoteViewsFactory onGetViewFactory(android.content.Intent intent);
+  }
+
+}
+
diff --git a/core/core-remoteviews/build.gradle b/core/core-remoteviews/build.gradle
new file mode 100644
index 0000000..12fd727
--- /dev/null
+++ b/core/core-remoteviews/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+
+import androidx.build.LibraryType
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api("androidx.annotation:annotation:1.2.0")
+    implementation(project(":core:core"))
+
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.kotlinTest)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+}
+
+androidx {
+    name = "AndroidX RemoteViews Support"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenVersion = LibraryVersions.CORE
+    mavenGroup = LibraryGroups.CORE
+    inceptionYear = "2021"
+    description = "AndroidX RemoteViews Support"
+}
diff --git a/core/core-remoteviews/lint-baseline.xml b/core/core-remoteviews/lint-baseline.xml
new file mode 100644
index 0000000..d145d5e
--- /dev/null
+++ b/core/core-remoteviews/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 7.1.0-alpha02" type="baseline" client="cli" name="Lint" variant="all" version="7.1.0-alpha02">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 16 (current min is 14): `android.appwidget.AppWidgetManager#bindAppWidgetIdIfAllowed`"
+        errorLine1="        val wasBound = appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, componentName)"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/core/widget/AppWidgetHostTestActivity.kt"
+            line="56"
+            column="41"/>
+    </issue>
+
+</issues>
diff --git a/core/core-remoteviews/src/androidTest/AndroidManifest.xml b/core/core-remoteviews/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..14dd4e9
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.core.remoteviews.test">
+    <uses-permission android:name="android.permission.BIND_APPWIDGET" />
+    <application>
+        <activity android:name="androidx.core.widget.AppWidgetHostTestActivity"/>
+
+        <receiver android:name="androidx.core.widget.TestAppWidgetProvider">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider"
+                android:resource="@xml/app_widget_info" />
+        </receiver>
+
+    </application>
+</manifest>
diff --git a/core/core-remoteviews/src/androidTest/java/androidx/core/widget/AppWidgetHostTestActivity.kt b/core/core-remoteviews/src/androidTest/java/androidx/core/widget/AppWidgetHostTestActivity.kt
new file mode 100644
index 0000000..c5124cb
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/java/androidx/core/widget/AppWidgetHostTestActivity.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 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 androidx.core.widget
+
+import android.app.Activity
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetHostView
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.os.Bundle
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.core.remoteviews.test.R
+import org.junit.Assert.fail
+
+/** Test activity that contains an [AppWidgetHost].  */
+public class AppWidgetHostTestActivity : Activity() {
+    private var mHost: AppWidgetHost? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        setContentView(R.layout.app_widget_host_activity)
+
+        mHost = AppWidgetHost(this, 1).also { it.startListening() }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mHost?.stopListening()
+        mHost?.deleteHost()
+        mHost = null
+    }
+
+    public fun bindAppWidget(): AppWidgetHostView {
+        val host = mHost ?: error("App widgets can only be bound while the activity is created")
+
+        val appWidgetManager = AppWidgetManager.getInstance(this)
+        val appWidgetId = host.allocateAppWidgetId()
+        val componentName = ComponentName(this, TestAppWidgetProvider::class.java)
+
+        val wasBound = appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, componentName)
+        if (!wasBound) {
+            fail("Failed to bind the app widget")
+        }
+
+        val info = appWidgetManager.getAppWidgetInfo(appWidgetId)
+        val hostView = host.createView(this, appWidgetId, info)
+        val contentFrame = findViewById<FrameLayout>(R.id.content)
+        contentFrame.addView(
+            hostView,
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT
+            )
+        )
+
+        return hostView
+    }
+}
diff --git a/core/core-remoteviews/src/androidTest/java/androidx/core/widget/RemoteViewsCompatTest.kt b/core/core-remoteviews/src/androidTest/java/androidx/core/widget/RemoteViewsCompatTest.kt
new file mode 100644
index 0000000..6afde6e
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/java/androidx/core/widget/RemoteViewsCompatTest.kt
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2021 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 androidx.core.widget
+
+import android.Manifest.permission
+import android.app.PendingIntent
+import android.app.UiAutomation
+import android.appwidget.AppWidgetHostView
+import android.appwidget.AppWidgetManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Parcel
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnDrawListener
+import android.widget.Adapter
+import android.widget.ListView
+import android.widget.RemoteViews
+import android.widget.TextView
+import androidx.core.os.BuildCompat
+import androidx.core.remoteviews.test.R
+import androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+
+@SdkSuppress(minSdkVersion = 29)
+@MediumTest
+public class RemoteViewsCompatTest {
+    private val mUsingBackport = !BuildCompat.isAtLeastS()
+    private val mContext = ApplicationProvider.getApplicationContext<Context>()
+    private val mPackageName = mContext.packageName
+    private val mAppWidgetManager = AppWidgetManager.getInstance(mContext)
+
+    @Rule
+    @JvmField
+    public val mActivityTestRule: ActivityScenarioRule<AppWidgetHostTestActivity> =
+        ActivityScenarioRule(AppWidgetHostTestActivity::class.java)
+
+    private lateinit var mRemoteViews: RemoteViews
+    private lateinit var mHostView: AppWidgetHostView
+    private var mAppWidgetId = 0
+
+    private val mUiAutomation: UiAutomation
+        get() = InstrumentationRegistry.getInstrumentation().uiAutomation
+
+    private val mListView: ListView
+        get() = mHostView.getChildAt(0) as ListView
+
+    @Before
+    public fun setUp() {
+        mUiAutomation.adoptShellPermissionIdentity(permission.BIND_APPWIDGET)
+
+        mActivityTestRule.scenario.onActivity { activity ->
+            mHostView = activity.bindAppWidget()
+        }
+
+        mAppWidgetId = mHostView.appWidgetId
+        mRemoteViews = RemoteViews(mPackageName, R.layout.remote_views_list)
+        mAppWidgetManager.updateAppWidget(mAppWidgetId, mRemoteViews)
+
+        // Wait until the remote views has been added to the host view.
+        observeDrawUntil { mHostView.childCount > 0 }
+    }
+
+    @After
+    public fun tearDown() {
+        mUiAutomation.dropShellPermissionIdentity()
+    }
+
+    @Test
+    public fun testParcelingAndUnparceling() {
+        val items = RemoteCollectionItems.Builder()
+            .setHasStableIds(true)
+            .setViewTypeCount(10)
+            .addItem(id = 3, RemoteViews(mPackageName, R.layout.list_view_row))
+            .addItem(id = 5, RemoteViews(mPackageName, R.layout.list_view_row2))
+            .build()
+
+        val parcel = Parcel.obtain()
+        val unparceled = try {
+            items.writeToParcel(parcel, /* flags= */ 0)
+            parcel.setDataPosition(0)
+            RemoteCollectionItems(parcel)
+        } finally {
+            parcel.recycle()
+        }
+
+        assertThat(unparceled.itemCount).isEqualTo(2)
+        assertThat(unparceled.getItemId(0)).isEqualTo(3)
+        assertThat(unparceled.getItemId(1)).isEqualTo(5)
+        assertThat(unparceled.getItemView(0).layoutId).isEqualTo(R.layout.list_view_row)
+        assertThat(unparceled.getItemView(1).layoutId).isEqualTo(R.layout.list_view_row2)
+        assertThat(unparceled.hasStableIds()).isTrue()
+        assertThat(unparceled.viewTypeCount).isEqualTo(10)
+    }
+
+    @Test
+    public fun testBuilder_empty() {
+        val items = RemoteCollectionItems.Builder().build()
+
+        assertThat(items.itemCount).isEqualTo(0)
+        assertThat(items.viewTypeCount).isEqualTo(1)
+        assertThat(items.hasStableIds()).isFalse()
+    }
+
+    @Test
+    public fun testBuilder_viewTypeCountUnspecified() {
+        val firstItem = RemoteViews(mPackageName, R.layout.list_view_row)
+        val secondItem = RemoteViews(mPackageName, R.layout.list_view_row2)
+        val items = RemoteCollectionItems.Builder()
+            .setHasStableIds(true)
+            .addItem(id = 3, firstItem)
+            .addItem(id = 5, secondItem)
+            .build()
+
+        assertThat(items.itemCount).isEqualTo(2)
+        assertThat(items.getItemId(0)).isEqualTo(3)
+        assertThat(items.getItemId(1)).isEqualTo(5)
+        assertThat(items.getItemView(0).layoutId).isEqualTo(R.layout.list_view_row)
+        assertThat(items.getItemView(1).layoutId).isEqualTo(R.layout.list_view_row2)
+        assertThat(items.hasStableIds()).isTrue()
+        // The view type count should be derived from the number of different layout ids if
+        // unspecified.
+        assertThat(items.viewTypeCount).isEqualTo(2)
+    }
+
+    @Test
+    public fun testBuilder_viewTypeCountSpecified() {
+        val firstItem = RemoteViews(mPackageName, R.layout.list_view_row)
+        val secondItem = RemoteViews(mPackageName, R.layout.list_view_row2)
+        val items = RemoteCollectionItems.Builder()
+            .addItem(id = 3, firstItem)
+            .addItem(id = 5, secondItem)
+            .setViewTypeCount(15)
+            .build()
+
+        assertThat(items.viewTypeCount).isEqualTo(15)
+    }
+
+    @Test
+    public fun testBuilder_repeatedIdsAndLayouts() {
+        val firstItem = RemoteViews(mPackageName, R.layout.list_view_row)
+        val secondItem = RemoteViews(mPackageName, R.layout.list_view_row)
+        val thirdItem = RemoteViews(mPackageName, R.layout.list_view_row)
+        val items = RemoteCollectionItems.Builder()
+            .setHasStableIds(false)
+            .addItem(id = 42, firstItem)
+            .addItem(id = 42, secondItem)
+            .addItem(id = 42, thirdItem)
+            .build()
+
+        assertThat(items.itemCount).isEqualTo(3)
+        assertThat(items.getItemId(0)).isEqualTo(42)
+        assertThat(items.getItemId(1)).isEqualTo(42)
+        assertThat(items.getItemId(2)).isEqualTo(42)
+        assertThat(items.getItemView(0)).isSameInstanceAs(firstItem)
+        assertThat(items.getItemView(1)).isSameInstanceAs(secondItem)
+        assertThat(items.getItemView(2)).isSameInstanceAs(thirdItem)
+        assertThat(items.hasStableIds()).isFalse()
+        assertThat(items.viewTypeCount).isEqualTo(1)
+    }
+
+    @Test
+    public fun testBuilder_viewTypeCountLowerThanLayoutCount() {
+        assertFailsWith(IllegalArgumentException::class) {
+            RemoteCollectionItems.Builder()
+                .setHasStableIds(true)
+                .setViewTypeCount(1)
+                .addItem(3, RemoteViews(mPackageName, R.layout.list_view_row))
+                .addItem(5, RemoteViews(mPackageName, R.layout.list_view_row2))
+                .build()
+        }
+    }
+
+    @Test
+    public fun testServiceIntent_hasSameUriForSameIds() {
+        val intent1 = RemoteViewsCompatService.createIntent(mContext, appWidgetId = 1, viewId = 42)
+        val intent2 = RemoteViewsCompatService.createIntent(mContext, appWidgetId = 1, viewId = 42)
+
+        assertThat(intent1.data).isEqualTo(intent2.data)
+    }
+
+    @Test
+    public fun testServiceIntent_hasDifferentUriForDifferentWidgetIds() {
+        val intent1 = RemoteViewsCompatService.createIntent(mContext, appWidgetId = 1, viewId = 42)
+        val intent2 = RemoteViewsCompatService.createIntent(mContext, appWidgetId = 2, viewId = 42)
+
+        assertThat(intent1.data).isNotEqualTo(intent2.data)
+    }
+
+    @Test
+    public fun testServiceIntent_hasDifferentUriForDifferentViewIds() {
+        val intent1 = RemoteViewsCompatService.createIntent(mContext, appWidgetId = 1, viewId = 42)
+        val intent2 = RemoteViewsCompatService.createIntent(mContext, appWidgetId = 1, viewId = 43)
+
+        assertThat(intent1.data).isNotEqualTo(intent2.data)
+    }
+
+    @Test
+    public fun testSetRemoteAdapter_emptyCollection() {
+        val items = RemoteCollectionItems.Builder().build()
+
+        RemoteViewsCompat.setRemoteAdapter(
+            mContext,
+            mRemoteViews,
+            mAppWidgetId,
+            R.id.list_view,
+            items
+        )
+        mAppWidgetManager.updateAppWidget(mAppWidgetId, mRemoteViews)
+
+        observeDrawUntil { mListView.adapter != null }
+
+        assertThat(mListView.childCount).isEqualTo(0)
+        assertThat(mListView.adapter.count).isEqualTo(0)
+        assertThat(mListView.adapter.viewTypeCount).isAtLeast(1)
+        assertThat(mListView.adapter.hasStableIds()).isFalse()
+    }
+
+    @Test
+    public fun testSetRemoteAdapter_withItems() {
+        val items = RemoteCollectionItems.Builder()
+            .setHasStableIds(true)
+            .addItem(id = 10, createTextRow("Hello"))
+            .addItem(id = 11, createTextRow("World"))
+            .build()
+
+        RemoteViewsCompat.setRemoteAdapter(
+            mContext,
+            mRemoteViews,
+            mAppWidgetId,
+            R.id.list_view,
+            items
+        )
+        mAppWidgetManager.updateAppWidget(mAppWidgetId, mRemoteViews)
+
+        observeDrawUntil { mListView.adapter != null && mListView.childCount == 2 }
+
+        val adapter = mListView.adapter
+        assertThat(adapter.count).isEqualTo(2)
+        assertThat(adapter.getItemViewType(1)).isEqualTo(adapter.getItemViewType(0))
+        assertThat(adapter.getItemId(0)).isEqualTo(10)
+        assertThat(adapter.getItemId(1)).isEqualTo(11)
+
+        assertThat(mListView.adapter.hasStableIds()).isTrue()
+        assertThat(mListView.childCount).isEqualTo(2)
+        assertThat(getListChildAt<TextView>(0).text.toString()).isEqualTo("Hello")
+        assertThat(getListChildAt<TextView>(1).text.toString()).isEqualTo("World")
+    }
+
+    @Test
+    public fun testSetRemoteAdapter_clickListener() {
+        val action = "my-action"
+        val receiver = TestBroadcastReceiver()
+        mContext.registerReceiver(receiver, IntentFilter(action))
+        val pendingIntent = PendingIntent.getBroadcast(
+            mContext,
+            0,
+            Intent(action).setPackage(mPackageName),
+            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+        )
+        mRemoteViews.setPendingIntentTemplate(R.id.list_view, pendingIntent)
+
+        val item2 = RemoteViews(mPackageName, R.layout.list_view_row2)
+        item2.setTextViewText(R.id.text, "Clickable")
+        item2.setOnClickFillInIntent(R.id.text, Intent().putExtra("my-extra", 42))
+
+        val items = RemoteCollectionItems.Builder()
+            .setHasStableIds(true)
+            .addItem(id = 10, createTextRow("Hello"))
+            .addItem(id = 11, createTextRow("World"))
+            .addItem(id = 12, item2)
+            .build()
+        RemoteViewsCompat.setRemoteAdapter(
+            mContext,
+            mRemoteViews,
+            mAppWidgetId,
+            R.id.list_view,
+            items
+        )
+        mAppWidgetManager.updateAppWidget(mAppWidgetId, mRemoteViews)
+        observeDrawUntil { mListView.adapter != null && mListView.childCount == 3 }
+
+        val adapter: Adapter = mListView.adapter
+        assertThat(adapter.count).isEqualTo(3)
+        assertThat(adapter.getItemViewType(0)).isEqualTo(adapter.getItemViewType(1))
+        assertThat(adapter.getItemViewType(0)).isNotEqualTo(adapter.getItemViewType(2))
+        assertThat(adapter.getItemId(0)).isEqualTo(10)
+        assertThat(adapter.getItemId(1)).isEqualTo(11)
+        assertThat(adapter.getItemId(2)).isEqualTo(12)
+        assertThat(adapter.hasStableIds()).isTrue()
+
+        assertThat(mListView.childCount).isEqualTo(3)
+        val textView2 = getListChildAt<ViewGroup>(2).getChildAt(0) as TextView
+        assertThat(getListChildAt<TextView>(0).text.toString()).isEqualTo("Hello")
+        assertThat(getListChildAt<TextView>(1).text.toString()).isEqualTo("World")
+        assertThat(textView2.text.toString()).isEqualTo("Clickable")
+
+        // View being clicked should launch the intent.
+        val receiverIntent = receiver.runAndAwaitIntentReceived {
+            textView2.performClick()
+        }
+        assertThat(receiverIntent.getIntExtra("my-extra", 0)).isEqualTo(42)
+        mContext.unregisterReceiver(receiver)
+    }
+
+    @Test
+    public fun testSetRemoteAdapter_multipleCalls() {
+        var items = RemoteCollectionItems.Builder()
+            .setHasStableIds(true)
+            .addItem(id = 10, createTextRow("Hello"))
+            .addItem(id = 11, createTextRow("World"))
+            .build()
+        RemoteViewsCompat.setRemoteAdapter(
+            mContext,
+            mRemoteViews,
+            mAppWidgetId,
+            R.id.list_view,
+            items
+        )
+        mAppWidgetManager.updateAppWidget(mAppWidgetId, mRemoteViews)
+        observeDrawUntil { mListView.adapter != null && mListView.childCount == 2 }
+
+        items = RemoteCollectionItems.Builder()
+            .setHasStableIds(true)
+            .addItem(id = 20, createTextRow("Bonjour"))
+            .addItem(id = 21, createTextRow("le"))
+            .addItem(id = 22, createTextRow("monde"))
+            .build()
+        RemoteViewsCompat.setRemoteAdapter(
+            mContext,
+            mRemoteViews,
+            mAppWidgetId,
+            R.id.list_view,
+            items
+        )
+        mAppWidgetManager.updateAppWidget(mAppWidgetId, mRemoteViews)
+        observeDrawUntil { mListView.childCount == 3 }
+
+        val adapter: Adapter = mListView.adapter
+        assertThat(adapter.count).isEqualTo(3)
+        assertThat(adapter.getItemId(0)).isEqualTo(20)
+        assertThat(adapter.getItemId(1)).isEqualTo(21)
+        assertThat(adapter.getItemId(2)).isEqualTo(22)
+
+        assertThat(mListView.childCount).isEqualTo(3)
+        assertThat(getListChildAt<TextView>(0).text.toString()).isEqualTo("Bonjour")
+        assertThat(getListChildAt<TextView>(1).text.toString()).isEqualTo("le")
+        assertThat(getListChildAt<TextView>(2).text.toString()).isEqualTo("monde")
+    }
+
+    private fun createTextRow(text: String): RemoteViews {
+        return RemoteViews(mPackageName, R.layout.list_view_row)
+            .also { it.setTextViewText(R.id.text, text) }
+    }
+
+    private fun observeDrawUntil(test: () -> Boolean) {
+        val latch = CountDownLatch(1)
+        val  {
+            if (test()) latch.countDown()
+        }
+
+        mActivityTestRule.scenario.onActivity {
+            mHostView.viewTreeObserver.addOnDrawListener(onDrawListener)
+        }
+
+        val countedDown = latch.await(5, TimeUnit.SECONDS)
+
+        mActivityTestRule.scenario.onActivity {
+            mHostView.viewTreeObserver.removeOnDrawListener(onDrawListener)
+        }
+
+        if (!countedDown && !test()) {
+            fail("Expected condition to be met within 5 seconds")
+        }
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    private fun <V : View> getListChildAt(position: Int): V {
+        return if (mUsingBackport) {
+            // When using RemoteViewsAdapter, an extra wrapper FrameLayout is added.
+            (mListView.getChildAt(position) as ViewGroup).getChildAt(0) as V
+        } else {
+            mListView.getChildAt(position) as V
+        }
+    }
+
+    private inner class TestBroadcastReceiver : BroadcastReceiver() {
+        private lateinit var mCountDownLatch: CountDownLatch
+
+        private var mIntent: Intent? = null
+
+        override fun onReceive(context: Context, intent: Intent) {
+            mIntent = intent
+            mCountDownLatch.countDown()
+        }
+
+        fun runAndAwaitIntentReceived(runnable: () -> Unit): Intent {
+            mCountDownLatch = CountDownLatch(1)
+
+            mActivityTestRule.scenario.onActivity { runnable() }
+
+            mCountDownLatch.await(5, TimeUnit.SECONDS)
+
+            return mIntent ?: fail("Expected intent to be received within five seconds")
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/core-remoteviews/src/androidTest/java/androidx/core/widget/TestAppWidgetProvider.kt b/core/core-remoteviews/src/androidTest/java/androidx/core/widget/TestAppWidgetProvider.kt
new file mode 100644
index 0000000..551595e
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/java/androidx/core/widget/TestAppWidgetProvider.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 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 androidx.core.widget
+
+import android.appwidget.AppWidgetProvider
+
+public class TestAppWidgetProvider : AppWidgetProvider()
\ No newline at end of file
diff --git a/core/core-remoteviews/src/androidTest/res/layout/app_widget_host_activity.xml b/core/core-remoteviews/src/androidTest/res/layout/app_widget_host_activity.xml
new file mode 100644
index 0000000..4ad8573
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/res/layout/app_widget_host_activity.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/core/core-remoteviews/src/androidTest/res/layout/list_view_row.xml b/core/core-remoteviews/src/androidTest/res/layout/list_view_row.xml
new file mode 100644
index 0000000..c7fda32
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/res/layout/list_view_row.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/text"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"/>
\ No newline at end of file
diff --git a/core/core-remoteviews/src/androidTest/res/layout/list_view_row2.xml b/core/core-remoteviews/src/androidTest/res/layout/list_view_row2.xml
new file mode 100644
index 0000000..2da2e68
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/res/layout/list_view_row2.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <TextView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/text"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"/>
+</FrameLayout>
diff --git a/core/core-remoteviews/src/androidTest/res/layout/remote_views_list.xml b/core/core-remoteviews/src/androidTest/res/layout/remote_views_list.xml
new file mode 100644
index 0000000..e6ce819
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/res/layout/remote_views_list.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/list_view"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/core/core-remoteviews/src/androidTest/res/xml/app_widget_info.xml b/core/core-remoteviews/src/androidTest/res/xml/app_widget_info.xml
new file mode 100644
index 0000000..a1f8eae
--- /dev/null
+++ b/core/core-remoteviews/src/androidTest/res/xml/app_widget_info.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="40dp"
+    android:minHeight="40dp"
+    android:initialLayout="@layout/remote_views_list"
+    android:resizeMode="horizontal|vertical"
+    android:widgetCategory="home_screen">
+</appwidget-provider>
\ No newline at end of file
diff --git a/core/core-remoteviews/src/main/AndroidManifest.xml b/core/core-remoteviews/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9a694b6
--- /dev/null
+++ b/core/core-remoteviews/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+<manifest package="androidx.core.remoteviews" xmlns:android="http://schemas.android.com/apk/res/android">
+    <application>
+      <service android:name="androidx.core.widget.RemoteViewsCompatService"
+            android:permission="android.permission.BIND_REMOTEVIEWS"/>
+    </application>
+</manifest>
diff --git a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
new file mode 100644
index 0000000..0cf7f8e
--- /dev/null
+++ b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompat.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2021 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 androidx.core.widget
+
+import android.annotation.SuppressLint
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.os.Parcel
+import android.os.Parcelable
+import android.widget.RemoteViews
+import androidx.annotation.DoNotInline
+import androidx.annotation.IdRes
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
+
+/**
+ * Helper for accessing features in [RemoteViews].
+ */
+public object RemoteViewsCompat {
+    /**
+     * Creates a simple Adapter for the widgetId and viewId specified. The viewId must point to an
+     * AdapterView, ie. [android.widget.ListView], [android.widget.GridView],
+     * [android.widget.StackView], or [android.widget.AdapterViewAnimator].
+     *
+     * This is a simpler but less flexible approach to populating collection widgets. Its use is
+     * encouraged for most scenarios, as long as the total memory within the list of RemoteViews
+     * is relatively small (ie. doesn't contain large or numerous Bitmaps, see
+     * [RemoteViews.setImageViewBitmap]). In the case of numerous images, the use of API is
+     * still possible by setting image URIs instead of Bitmaps, see [RemoteViews.setImageViewUri].
+     *
+     * If you use this API, you should not call
+     * [AppWidgetManager.notifyAppWidgetViewDataChanged] and should instead update
+     * your app widget, calling this method with the new [RemoteCollectionItems].
+     *
+     * @param context     The [Context] of the app providing the widget.
+     * @param remoteViews The [RemoteViews] to receive the adapter.
+     * @param appWidgetId the id of the widget for which the adapter is being set.
+     * @param viewId      The id of the [android.widget.AdapterView].
+     * @param items       The items to display in the [android.widget.AdapterView].
+     */
+    @JvmStatic
+    public fun setRemoteAdapter(
+        context: Context,
+        remoteViews: RemoteViews,
+        appWidgetId: Int,
+        @IdRes viewId: Int,
+        items: RemoteCollectionItems
+    ) {
+        if (BuildCompat.isAtLeastS()) {
+            try {
+                Api31Impl.setRemoteAdapter(remoteViews, viewId, items)
+                return
+            } catch (e: LinkageError) {
+                // This will occur if the API doesn't exist yet on this version of S. We can simply
+                // fall back to the approach we use on pre-S devices.
+            }
+        }
+        val intent = RemoteViewsCompatService.createIntent(context, appWidgetId, viewId)
+        check(context.packageManager.resolveService(intent, /* flags= */ 0) != null) {
+            "RemoteViewsCompatService could not be resolved, ensure that you have declared it in " +
+                "your app manifest."
+        }
+        remoteViews.setRemoteAdapter(viewId, intent)
+        RemoteViewsCompatService.saveItems(context, appWidgetId, viewId, items)
+        AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, viewId)
+    }
+
+    /** Representation of a fixed list of items to be displayed in a RemoteViews collection.  */
+    public class RemoteCollectionItems {
+        private val mIds: LongArray
+        private val mViews: Array<RemoteViews>
+        private val mHasStableIds: Boolean
+        private val mViewTypeCount: Int
+
+        internal constructor(
+            ids: LongArray,
+            views: Array<RemoteViews>,
+            hasStableIds: Boolean,
+            viewTypeCount: Int
+        ) {
+            mIds = ids
+            mViews = views
+            mHasStableIds = hasStableIds
+            mViewTypeCount = viewTypeCount
+
+            require(ids.size == views.size) {
+                "RemoteCollectionItems has different number of ids and views"
+            }
+            require(viewTypeCount >= 1) { "View type count must be >= 1" }
+
+            val layoutIdCount = views.map { it.layoutId }.distinct().count()
+            require(layoutIdCount <= viewTypeCount) {
+                "View type count is set to $viewTypeCount, but the collection contains " +
+                    "$layoutIdCount different layout ids"
+            }
+        }
+
+        /** @hide */
+        internal constructor(parcel: Parcel) {
+            val length = parcel.readInt()
+            mIds = LongArray(length)
+            parcel.readLongArray(mIds)
+            mViews = parcel.readNonNullTypedArray(length, RemoteViews.CREATOR)
+            mHasStableIds = parcel.readInt() == 1
+            mViewTypeCount = parcel.readInt()
+        }
+
+        /** @hide */
+        internal fun writeToParcel(dest: Parcel, flags: Int) {
+            dest.writeInt(mIds.size)
+            dest.writeLongArray(mIds)
+            dest.writeTypedArray(mViews, flags)
+            dest.writeInt(if (mHasStableIds) 1 else 0)
+            dest.writeInt(mViewTypeCount)
+        }
+
+        /**
+         * Returns the id for [position]. See [hasStableIds] for whether this id should be
+         * considered meaningful across collection updates.
+         *
+         * @return Id for the position.
+         */
+        public fun getItemId(position: Int): Long = mIds[position]
+
+        /**
+         * Returns the [RemoteViews] to display at [position].
+         *
+         * @return RemoteViews for the position.
+         */
+        public fun getItemView(position: Int): RemoteViews = mViews[position]
+
+        /**
+         * Returns the number of elements in the collection.
+         *
+         * @return Count of items.
+         */
+        public val itemCount: Int
+            get() = mIds.size
+
+        /**
+         * Returns the view type count for the collection when used in an adapter
+         *
+         * @return Count of view types for the collection when used in an adapter.
+         * @see android.widget.Adapter.getViewTypeCount
+         */
+        public val viewTypeCount: Int
+            get() = mViewTypeCount
+
+        /**
+         * Indicates whether the item ids are stable across changes to the underlying data.
+         *
+         * @return True if the same id always refers to the same object.
+         * @see android.widget.Adapter.hasStableIds
+         */
+        public fun hasStableIds(): Boolean = mHasStableIds
+
+        /** Builder class for [RemoteCollectionItems] objects. */
+        public class Builder {
+            private val mIds = arrayListOf<Long>()
+            private val mViews = arrayListOf<RemoteViews>()
+            private var mHasStableIds = false
+            private var mViewTypeCount = 0
+
+            /**
+             * Adds a [RemoteViews] to the collection.
+             *
+             * @param id   Id to associate with the row. Use [.setHasStableIds] to
+             * indicate that ids are stable across changes to the collection.
+             * @param view RemoteViews to display for the row.
+             */
+            // Covered by getItemId, getItemView, getItemCount.
+            @SuppressLint("MissingGetterMatchingBuilder")
+            public fun addItem(id: Long, view: RemoteViews): Builder {
+                mIds.add(id)
+                mViews.add(view)
+                return this
+            }
+
+            /**
+             * Sets whether the item ids are stable across changes to the underlying data.
+             *
+             * @see android.widget.Adapter.hasStableIds
+             */
+            public fun setHasStableIds(hasStableIds: Boolean): Builder {
+                mHasStableIds = hasStableIds
+                return this
+            }
+
+            /**
+             * Sets the view type count for the collection when used in an adapter. This can be set
+             * to the maximum number of different layout ids that will be used by RemoteViews in
+             * this collection.
+             *
+             * If this value is not set, then a value will be inferred from the provided items. As
+             * a result, the adapter may need to be recreated when the list is updated with
+             * previously unseen RemoteViews layouts for new items.
+             *
+             * @see android.widget.Adapter.getViewTypeCount
+             */
+            public fun setViewTypeCount(viewTypeCount: Int): Builder {
+                mViewTypeCount = viewTypeCount
+                return this
+            }
+
+            /** Creates the [RemoteCollectionItems] defined by this builder.  */
+            public fun build(): RemoteCollectionItems {
+                if (mViewTypeCount < 1) {
+                    // If a view type count wasn't specified, set it to be the number of distinct
+                    // layout ids used in the items.
+                    mViewTypeCount = mViews.map { it.layoutId }.distinct().count()
+                }
+                return RemoteCollectionItems(
+                    mIds.toLongArray(),
+                    mViews.toTypedArray(),
+                    mHasStableIds,
+                    maxOf(mViewTypeCount, 1)
+                )
+            }
+        }
+
+        private companion object {
+            /** Reads a non-null array of [T] of [size] from the [Parcel]. */
+            inline fun <reified T : Any> Parcel.readNonNullTypedArray(
+                size: Int,
+                creator: Parcelable.Creator<T>
+            ): Array<T> {
+                val array = arrayOfNulls<T?>(size)
+                readTypedArray(array, creator)
+                return array.requireNoNulls()
+            }
+        }
+    }
+
+    /**
+     * Version-specific static inner class to avoid verification errors that negatively affect
+     * run-time performance.
+     */
+    @RequiresApi(31)
+    private object Api31Impl {
+        @DoNotInline
+        fun setRemoteAdapter(remoteViews: RemoteViews, viewId: Int, items: RemoteCollectionItems) {
+            remoteViews.setRemoteAdapter(viewId, toPlatformCollectionItems(items))
+        }
+
+        /**
+         * Returns a [RemoteViews.RemoteCollectionItems] equivalent to this [RemoteCollectionItems].
+         */
+        @DoNotInline
+        private fun toPlatformCollectionItems(
+            items: RemoteCollectionItems
+        ): RemoteViews.RemoteCollectionItems {
+            return RemoteViews.RemoteCollectionItems.Builder()
+                .setHasStableIds(items.hasStableIds())
+                .setViewTypeCount(items.viewTypeCount)
+                .also { builder ->
+                    repeat(items.itemCount) { index ->
+                        builder.addItem(items.getItemId(index), items.getItemView(index))
+                    }
+                }
+                .build()
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
new file mode 100644
index 0000000..46b257b
--- /dev/null
+++ b/core/core-remoteviews/src/main/java/androidx/core/widget/RemoteViewsCompatService.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2021 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 androidx.core.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Base64
+import android.util.Log
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService
+import androidx.annotation.RestrictTo
+import androidx.core.content.pm.PackageInfoCompat
+import androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems
+
+/**
+ * [RemoteViewsService] to provide [RemoteViews] set using [RemoteViewsCompat.setRemoteAdapter].
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class RemoteViewsCompatService : RemoteViewsService() {
+    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
+        val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
+        check(appWidgetId != -1) { "No app widget id was present in the intent" }
+
+        val viewId = intent.getIntExtra(EXTRA_VIEW_ID, -1)
+        check(viewId != -1) { "No view id was present in the intent" }
+
+        return RemoteViewsCompatServiceViewFactory(this, appWidgetId, viewId)
+    }
+
+    private class RemoteViewsCompatServiceViewFactory(
+        private val mContext: Context,
+        private val mAppWidgetId: Int,
+        private val mViewId: Int
+    ) : RemoteViewsFactory {
+        private var mItems = EMPTY
+
+        override fun onCreate() = loadData()
+
+        override fun onDataSetChanged() = loadData()
+
+        private fun loadData() {
+            mItems = RemoteViewsCompatServiceData.load(mContext, mAppWidgetId, mViewId) ?: EMPTY
+        }
+
+        override fun onDestroy() {}
+
+        override fun getCount() = mItems.itemCount
+
+        override fun getViewAt(position: Int) = mItems.getItemView(position)
+
+        override fun getLoadingView() = null
+
+        override fun getViewTypeCount() = mItems.viewTypeCount
+
+        override fun getItemId(position: Int): Long {
+            return mItems.getItemId(position)
+        }
+
+        override fun hasStableIds() = mItems.hasStableIds()
+
+        companion object {
+            private val EMPTY = RemoteCollectionItems(
+                ids = longArrayOf(),
+                views = emptyArray(),
+                hasStableIds = false,
+                viewTypeCount = 1
+            )
+        }
+    }
+
+    /**
+     * Wrapper around a serialized [RemoteCollectionItems] with metadata about the versions
+     * of Android and the app when the items were created.
+     *
+     * Our method of serialization and deserialization is to marshall and unmarshall the items to
+     * a byte array using their [Parcelable] implementation. As Parcelable definitions can change
+     * over time, it is not safe to do this across different versions of a package or Android
+     * itself. However, as app widgets are recreated on reboot or when a package is updated, this
+     * is not a problem for the approach used here.
+     *
+     * This data wrapper stores the current build of Android and the provider app at the time of
+     * serialization and deserialization is only attempted in [load] if both are the same as at
+     * the time of serialization.
+     */
+    private class RemoteViewsCompatServiceData {
+        private val mItemsBytes: ByteArray
+        private val mBuildVersion: String
+        private val mAppVersion: Long
+
+        private constructor(
+            itemsBytes: ByteArray,
+            buildVersion: String,
+            appVersion: Long
+        ) {
+            mItemsBytes = itemsBytes
+            mBuildVersion = buildVersion
+            mAppVersion = appVersion
+        }
+
+        constructor(parcel: Parcel) {
+            val length = parcel.readInt()
+            mItemsBytes = ByteArray(length)
+            parcel.readByteArray(mItemsBytes)
+            mBuildVersion = parcel.readString()!!
+            mAppVersion = parcel.readLong()
+        }
+
+        fun writeToParcel(dest: Parcel) {
+            dest.writeInt(mItemsBytes.size)
+            dest.writeByteArray(mItemsBytes)
+            dest.writeString(mBuildVersion)
+            dest.writeLong(mAppVersion)
+        }
+
+        fun save(context: Context, appWidgetId: Int, viewId: Int) {
+            getPrefs(context)
+                .edit()
+                .putString(
+                    getKey(appWidgetId, viewId),
+                    serializeToHexString { parcel, _ -> writeToParcel(parcel) }
+                )
+                .apply()
+        }
+
+        companion object {
+            private const val PREFS_FILENAME = "androidx.core.widget.prefs.RemoteViewsCompat"
+
+            internal fun getKey(appWidgetId: Int, viewId: Int): String {
+                return "$appWidgetId:$viewId"
+            }
+
+            internal fun getPrefs(context: Context): SharedPreferences {
+                return context.getSharedPreferences(PREFS_FILENAME, MODE_PRIVATE)
+            }
+
+            fun create(
+                context: Context,
+                items: RemoteCollectionItems
+            ): RemoteViewsCompatServiceData {
+                val versionCode = getVersionCode(context)
+                check(versionCode != null) { "Couldn't obtain version code for app" }
+                return RemoteViewsCompatServiceData(
+                    itemsBytes = serializeToBytes(items::writeToParcel),
+                    buildVersion = Build.VERSION.INCREMENTAL,
+                    appVersion = versionCode
+                )
+            }
+
+            /**
+             * Returns the stored [RemoteCollectionItems] for the widget/view id, or null if
+             * it couldn't be retrieved for any reason.
+             */
+            internal fun load(
+                context: Context,
+                appWidgetId: Int,
+                viewId: Int
+            ): RemoteCollectionItems? {
+                val prefs = getPrefs(context)
+                val hexString = prefs.getString(getKey(appWidgetId, viewId), /* defValue= */ null)
+                if (hexString == null) {
+                    Log.w(TAG, "No collection items were stored for widget $appWidgetId")
+                    return null
+                }
+                val data = deserializeFromHexString(hexString) { RemoteViewsCompatServiceData(it) }
+                if (Build.VERSION.INCREMENTAL != data.mBuildVersion) {
+                    Log.w(
+                        TAG,
+                        "Android version code has changed, not using stored collection items for " +
+                            "widget $appWidgetId"
+                    )
+                    return null
+                }
+                val versionCode = getVersionCode(context)
+                if (versionCode == null) {
+                    Log.w(
+                        TAG,
+                        "Couldn't get version code, not using stored collection items for widget " +
+                            appWidgetId
+                    )
+                    return null
+                }
+                if (versionCode != data.mAppVersion) {
+                    Log.w(
+                        TAG,
+                        "App version code has changed, not using stored collection items for " +
+                            "widget $appWidgetId"
+                    )
+                    return null
+                }
+                return try {
+                    deserializeFromBytes(data.mItemsBytes) { RemoteCollectionItems(it) }
+                } catch (t: Throwable) {
+                    Log.e(
+                        TAG,
+                        "Unable to deserialize stored collection items for widget $appWidgetId",
+                        t
+                    )
+                    null
+                }
+            }
+
+            internal fun getVersionCode(context: Context): Long? {
+                val packageManager = context.packageManager
+                val packageInfo = try {
+                    packageManager.getPackageInfo(context.packageName, 0)
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.e(TAG, "Couldn't retrieve version code for " + context.packageManager, e)
+                    return null
+                }
+                return PackageInfoCompat.getLongVersionCode(packageInfo)
+            }
+
+            internal fun serializeToHexString(parcelable: (Parcel, Int) -> Unit): String {
+                return Base64.encodeToString(serializeToBytes(parcelable), Base64.DEFAULT)
+            }
+
+            internal fun serializeToBytes(parcelable: (Parcel, Int) -> Unit): ByteArray {
+                val parcel = Parcel.obtain()
+                return try {
+                    parcel.setDataPosition(0)
+                    parcelable(parcel, 0)
+                    parcel.marshall()
+                } finally {
+                    parcel.recycle()
+                }
+            }
+
+            internal fun <P> deserializeFromHexString(
+                hexString: String,
+                creator: (Parcel) -> P,
+            ): P {
+                return deserializeFromBytes(Base64.decode(hexString, Base64.DEFAULT), creator)
+            }
+
+            internal fun <P> deserializeFromBytes(
+                bytes: ByteArray,
+                creator: (Parcel) -> P,
+            ): P {
+                val parcel = Parcel.obtain()
+                return try {
+                    parcel.unmarshall(bytes, 0, bytes.size)
+                    parcel.setDataPosition(0)
+                    creator(parcel)
+                } finally {
+                    parcel.recycle()
+                }
+            }
+        }
+    }
+
+    internal companion object {
+        private const val TAG = "RemoteViewsCompatServic"
+        private const val EXTRA_VIEW_ID = "androidx.core.widget.extra.view_id"
+
+        /**
+         * Returns an intent use with [RemoteViews.setRemoteAdapter]. These intents
+         * are uniquely identified by the [appWidgetId] and [viewId].
+         */
+        fun createIntent(
+            context: Context,
+            appWidgetId: Int,
+            viewId: Int
+        ): Intent {
+            return Intent(context, RemoteViewsCompatService::class.java)
+                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+                .putExtra(EXTRA_VIEW_ID, viewId)
+                .also { intent ->
+                    // Set a data Uri to disambiguate Intents for different widget/view ids.
+                    intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
+                }
+        }
+
+        /**
+         * Stores [items] to the disk to be used by a [RemoteViewsCompatService] for the same
+         * [appWidgetId] and [viewId].
+         */
+        fun saveItems(
+            context: Context,
+            appWidgetId: Int,
+            viewId: Int,
+            items: RemoteCollectionItems
+        ) {
+            RemoteViewsCompatServiceData.create(context, items).save(context, appWidgetId, viewId)
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java b/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java
index 38f250af..d739e9b 100644
--- a/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java
+++ b/core/core-role/src/main/java/androidx/core/role/RoleManagerCompat.java
@@ -56,6 +56,14 @@
      * </activity>
      * }</pre>
      * The application will be able to handle that intent by default.
+     * <p>
+     * Apps that hold this role are allowed to start activities in response to notification clicks
+     * or notification action clicks when targeting {@link android.os.Build.VERSION_CODES#S} to give
+     * browsers time to adapt. This is temporary and browsers will be subjected to the same
+     * trampoline restrictions at some point in future releases. For more details on those
+     * restrictions see {@link android.app.Notification.Builder#setContentIntent(PendingIntent)} and
+     * {@link android.app.Notification.Action.Builder#Builder(android.graphics.drawable.Icon,
+     * java.lang.CharSequence, android.app.PendingIntent)}.
      *
      * @see android.content.Intent#CATEGORY_APP_BROWSER
      */
diff --git a/core/core-splashscreen/OWNERS b/core/core-splashscreen/OWNERS
new file mode 100644
index 0000000..831d512
--- /dev/null
+++ b/core/core-splashscreen/OWNERS
@@ -0,0 +1,8 @@
+caen@google.com
+jjaggi@google.com
+cinek@google.com
+yaraki@google.com
+chrisbanes@google.com
+wilsonshih@google.com
+
+
diff --git a/core/core-splashscreen/api/current.txt b/core/core-splashscreen/api/current.txt
new file mode 100644
index 0000000..898f855
--- /dev/null
+++ b/core/core-splashscreen/api/current.txt
@@ -0,0 +1,36 @@
+// Signature format: 4.0
+package androidx.core.splashscreen {
+
+  public final class SplashScreen {
+    method public static androidx.core.splashscreen.SplashScreen installSplashScreen(android.app.Activity);
+    method public void setKeepVisibleCondition(androidx.core.splashscreen.SplashScreen.KeepOnScreenCondition condition);
+    method public void setOnExitAnimationListener(androidx.core.splashscreen.SplashScreen.OnExitAnimationListener listener);
+    field public static final androidx.core.splashscreen.SplashScreen.Companion Companion;
+  }
+
+  public static final class SplashScreen.Companion {
+    method public androidx.core.splashscreen.SplashScreen installSplashScreen(android.app.Activity);
+  }
+
+  public static fun interface SplashScreen.KeepOnScreenCondition {
+    method @MainThread public boolean shouldKeepOnScreen();
+  }
+
+  public static fun interface SplashScreen.OnExitAnimationListener {
+    method @MainThread public void onSplashScreenExit(androidx.core.splashscreen.SplashScreenViewProvider splashScreenViewProvider);
+  }
+
+  public final class SplashScreenViewProvider {
+    method public long getIconAnimationDurationMillis();
+    method public long getIconAnimationStartMillis();
+    method public android.view.View getIconView();
+    method public android.view.View getView();
+    method public void remove();
+    property public final long iconAnimationDurationMillis;
+    property public final long iconAnimationStartMillis;
+    property public final android.view.View iconView;
+    property public final android.view.View view;
+  }
+
+}
+
diff --git a/core/core-splashscreen/api/public_plus_experimental_current.txt b/core/core-splashscreen/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..898f855
--- /dev/null
+++ b/core/core-splashscreen/api/public_plus_experimental_current.txt
@@ -0,0 +1,36 @@
+// Signature format: 4.0
+package androidx.core.splashscreen {
+
+  public final class SplashScreen {
+    method public static androidx.core.splashscreen.SplashScreen installSplashScreen(android.app.Activity);
+    method public void setKeepVisibleCondition(androidx.core.splashscreen.SplashScreen.KeepOnScreenCondition condition);
+    method public void setOnExitAnimationListener(androidx.core.splashscreen.SplashScreen.OnExitAnimationListener listener);
+    field public static final androidx.core.splashscreen.SplashScreen.Companion Companion;
+  }
+
+  public static final class SplashScreen.Companion {
+    method public androidx.core.splashscreen.SplashScreen installSplashScreen(android.app.Activity);
+  }
+
+  public static fun interface SplashScreen.KeepOnScreenCondition {
+    method @MainThread public boolean shouldKeepOnScreen();
+  }
+
+  public static fun interface SplashScreen.OnExitAnimationListener {
+    method @MainThread public void onSplashScreenExit(androidx.core.splashscreen.SplashScreenViewProvider splashScreenViewProvider);
+  }
+
+  public final class SplashScreenViewProvider {
+    method public long getIconAnimationDurationMillis();
+    method public long getIconAnimationStartMillis();
+    method public android.view.View getIconView();
+    method public android.view.View getView();
+    method public void remove();
+    property public final long iconAnimationDurationMillis;
+    property public final long iconAnimationStartMillis;
+    property public final android.view.View iconView;
+    property public final android.view.View view;
+  }
+
+}
+
diff --git a/core/core-splashscreen/api/res-current.txt b/core/core-splashscreen/api/res-current.txt
new file mode 100644
index 0000000..f751c82
--- /dev/null
+++ b/core/core-splashscreen/api/res-current.txt
@@ -0,0 +1,5 @@
+attr postSplashScreenTheme
+attr windowSplashScreenAnimatedIcon
+attr windowSplashScreenAnimationDuration
+attr windowSplashScreenBackground
+style Theme_SplashScreen
diff --git a/core/core-splashscreen/api/restricted_current.txt b/core/core-splashscreen/api/restricted_current.txt
new file mode 100644
index 0000000..898f855
--- /dev/null
+++ b/core/core-splashscreen/api/restricted_current.txt
@@ -0,0 +1,36 @@
+// Signature format: 4.0
+package androidx.core.splashscreen {
+
+  public final class SplashScreen {
+    method public static androidx.core.splashscreen.SplashScreen installSplashScreen(android.app.Activity);
+    method public void setKeepVisibleCondition(androidx.core.splashscreen.SplashScreen.KeepOnScreenCondition condition);
+    method public void setOnExitAnimationListener(androidx.core.splashscreen.SplashScreen.OnExitAnimationListener listener);
+    field public static final androidx.core.splashscreen.SplashScreen.Companion Companion;
+  }
+
+  public static final class SplashScreen.Companion {
+    method public androidx.core.splashscreen.SplashScreen installSplashScreen(android.app.Activity);
+  }
+
+  public static fun interface SplashScreen.KeepOnScreenCondition {
+    method @MainThread public boolean shouldKeepOnScreen();
+  }
+
+  public static fun interface SplashScreen.OnExitAnimationListener {
+    method @MainThread public void onSplashScreenExit(androidx.core.splashscreen.SplashScreenViewProvider splashScreenViewProvider);
+  }
+
+  public final class SplashScreenViewProvider {
+    method public long getIconAnimationDurationMillis();
+    method public long getIconAnimationStartMillis();
+    method public android.view.View getIconView();
+    method public android.view.View getView();
+    method public void remove();
+    property public final long iconAnimationDurationMillis;
+    property public final long iconAnimationStartMillis;
+    property public final android.view.View iconView;
+    property public final android.view.View view;
+  }
+
+}
+
diff --git a/core/core-splashscreen/build.gradle b/core/core-splashscreen/build.gradle
new file mode 100644
index 0000000..70b0d81
--- /dev/null
+++ b/core/core-splashscreen/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+        testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
+    }
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+
+    implementation("androidx.annotation:annotation:1.2.0")
+
+    androidTestImplementation(project(":test:screenshot:test-screenshot"))
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.testUiautomator)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(project(":appcompat:appcompat"))
+}
+
+androidx {
+    name = "SplashScreen"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenVersion = LibraryVersions.CORE_SPLASHSCREEN
+    mavenGroup = LibraryGroups.CORE
+    inceptionYear = "2021"
+    description = "This library provides the compatibility APIs for SplashScreen " +
+            "and helper method to enable a splashscreen on devices prior Android 12"
+}
diff --git a/core/core-splashscreen/core-splashscreen-sample/build.gradle b/core/core-splashscreen/core-splashscreen-sample/build.gradle
new file mode 100644
index 0000000..0128b8c
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.core.splashscreen.sample"
+        minSdkVersion 21
+    }
+}
+
+dependencies {
+    implementation(project(":appcompat:appcompat"))
+    implementation project(":annotation:annotation")
+    implementation(project(":core:core-splashscreen"))
+    implementation(project(":core:core-ktx"))
+    compileOnly(project(":annotation:annotation-sampled"))
+}
+
+androidx {
+    name = "AndroidX Splashscreen Samples"
+    publish = Publish.NONE
+    mavenVersion = LibraryVersions.CORE_SPLASHSCREEN
+    mavenGroup = LibraryGroups.CORE
+    inceptionYear = "2021"
+    description = "Sample for the AndoridX Splashscreen library"
+}
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/AndroidManifest.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..747c224
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.core.splashscreen.sample">
+
+    <application
+        android:icon="@drawable/ic_launcher"
+        android:theme="@style/Theme.App.Starting"
+        android:label="@string/app_name">
+        <activity android:name=".SplashScreenSampleActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/java/androidx/core/splashscreen/sample/SplashScreenSampleActivity.kt b/core/core-splashscreen/core-splashscreen-sample/src/main/java/androidx/core/splashscreen/sample/SplashScreenSampleActivity.kt
new file mode 100644
index 0000000..2cc239c
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/java/androidx/core/splashscreen/sample/SplashScreenSampleActivity.kt
@@ -0,0 +1,173 @@
+/*
+ * 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 androidx.core.splashscreen.sample
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.Process
+import android.view.View
+import android.view.animation.DecelerateInterpolator
+import android.widget.Button
+import android.widget.LinearLayout
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.animation.doOnEnd
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.splashscreen.SplashScreenViewProvider
+import androidx.core.view.WindowCompat
+import androidx.core.view.postDelayed
+import androidx.interpolator.view.animation.FastOutLinearInInterpolator
+
+@RequiresApi(21)
+class SplashScreenSampleActivity : AppCompatActivity() {
+
+    private var appReady = false
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // This activity will be handling the splash screen transition
+        val splashScreen = installSplashScreen()
+
+        // The splashscreen goes edge to edge, so for a smooth transition to our app, we also
+        // want to draw edge to edge.
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+
+        // The content view needs to set before calling setOnExitAnimationListener
+        // to ensure that the SplashScreenView is attach to the right view root.
+        setContentView(R.layout.main_activity)
+
+        // (Optional) We can keep the splash screen visible until our app is ready.
+        splashScreen.setKeepVisibleCondition { !appReady }
+
+        // (Optional) Setting an OnExitAnimationListener on the SplashScreen indicates
+        // to the system that the application will handle the exit animation.
+        // The listener will be called once the app is ready.
+        splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
+            onSplashScreenExit(splashScreenViewProvider)
+        }
+
+        /* The code below is only for demo purposes */
+        // Create some artificial delay to simulate some local database fetch for example
+        Handler(Looper.getMainLooper())
+            .postDelayed({ appReady = true }, (MOCK_DELAY).toLong())
+
+        // Just a convenient button in our App to kill its process so we can play with the
+        // splashscreen again and again.
+        setupKillButton()
+    }
+
+    /**
+     * Handles the transition from the splash screen to the application
+     */
+    private fun onSplashScreenExit(splashScreenViewProvider: SplashScreenViewProvider) {
+        val accelerateInterpolator = FastOutLinearInInterpolator()
+        val splashScreenView = splashScreenViewProvider.view
+        val iconView = splashScreenViewProvider.iconView
+
+        // We'll change the alpha of the main view
+        val alpha = ObjectAnimator.ofFloat(splashScreenView, View.ALPHA, 1f, 0f)
+        alpha.duration = SPLASHSCREEN_ALPHA_ANIMATION_DURATION.toLong()
+        alpha.interpolator = accelerateInterpolator
+
+        // And we translate the icon down
+        val translationY = ObjectAnimator.ofFloat(
+            iconView,
+            View.TRANSLATION_Y,
+            iconView.translationY,
+            splashScreenView.height.toFloat()
+        )
+        translationY.duration = SPLASHSCREEN_TY_ANIMATION_DURATION.toLong()
+        translationY.interpolator = accelerateInterpolator
+
+        // To get fancy, we'll also animate our content
+        val marginAnimator = createContentAnimation()
+
+        // And we play all of the animation together
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(translationY, alpha, marginAnimator)
+
+        // Once the application is finished, we remove the splash screen from our view
+        // hierarchy.
+        animatorSet.doOnEnd { splashScreenViewProvider.remove() }
+
+        if (WAIT_FOR_AVD_TO_FINISH) {
+            waitForAnimatedIconToFinish(splashScreenViewProvider, splashScreenView, animatorSet)
+        } else {
+            animatorSet.start()
+        }
+    }
+
+    /**
+     * Wait until the AVD animation is finished before starting the splash screen dismiss animation
+     */
+    private fun waitForAnimatedIconToFinish(
+        splashScreenViewProvider: SplashScreenViewProvider,
+        view: View,
+        animatorSet: AnimatorSet
+    ) {
+        // If we want to wait for our Animated Vector Drawable to finish animating, we can compute
+        // the remaining time to delay the start of the exit animation
+        val delayMillis: Long = (
+            splashScreenViewProvider.iconAnimationStartMillis +
+                splashScreenViewProvider.iconAnimationDurationMillis
+            ) - System.currentTimeMillis()
+        view.postDelayed(delayMillis) { animatorSet.start() }
+    }
+
+    /**
+     * Animates the content of the app in sync with the splash screen
+     */
+    private fun createContentAnimation(): ValueAnimator {
+        val marginStart = resources.getDimension(R.dimen.content_animation_margin_start)
+        val marginEnd = resources.getDimension(R.dimen.content_animation_margin_end)
+        val marginAnimator = ValueAnimator.ofFloat(marginStart, marginEnd)
+        marginAnimator.addUpdateListener { valueAnimator: ValueAnimator ->
+            val container = findViewById<LinearLayout>(R.id.container)
+            val marginTop = (valueAnimator.animatedValue as Float)
+            for (i in 0 until container.childCount) {
+                val child = container.getChildAt(i)
+                child.translationY = marginTop * (i + 1)
+            }
+        }
+        marginAnimator.interpolator = DecelerateInterpolator()
+        marginAnimator.duration = MARGIN_ANIMATION_DURATION.toLong()
+        return marginAnimator
+    }
+
+    private fun setupKillButton() {
+        findViewById<Button>(R.id.close_app).setOnClickListener {
+            finishAndRemoveTask()
+
+            // Don't do that in real life.
+            // For the sake of this demo app, we kill the process so the next time the app is
+            // launched, it will be a cold start and the splash screen will be visible.
+            Process.killProcess(Process.myPid())
+        }
+    }
+
+    private companion object {
+        const val MOCK_DELAY = 200
+        const val MARGIN_ANIMATION_DURATION = 800
+        const val SPLASHSCREEN_ALPHA_ANIMATION_DURATION = 500
+        const val SPLASHSCREEN_TY_ANIMATION_DURATION = 500
+        const val WAIT_FOR_AVD_TO_FINISH = false
+    }
+}
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable-v26/ic_launcher.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable-v26/ic_launcher.xml
new file mode 100644
index 0000000..d4d3966
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable-v26/ic_launcher.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/logo_background" />
+    <foreground android:drawable="@drawable/foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable-v31/splashscreen_icon.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable-v31/splashscreen_icon.xml
new file mode 100644
index 0000000..f37aee7
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable-v31/splashscreen_icon.xml
@@ -0,0 +1,74 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="108dp"
+            android:height="108dp"
+            android:viewportHeight="1200"
+            android:viewportWidth="1200">
+
+            <group
+                android:translateX="200"
+                android:translateY="200">
+                <clip-path android:pathData="m596.328,310.906c22.484,-39.699 46.513,-78.699 68.084,-118.812 4.258,-14.364 -17.654,-23.707 -25.102,-10.703 -22.603,38.973 -45.139,77.986 -67.716,116.974 -107.224,-48.772 -235.192,-48.772 -342.415,0 -23.491,-39.715 -45.567,-80.447 -69.895,-119.56 -10.384,-10.813 -29.373,3.556 -21.753,16.461 22.277,38.562 44.614,77.091 66.914,115.64C88.641,372.042 9.442,494.721 0,625c266.667,0 533.333,0 800,0C790.261,494.557 711.967,372.81 596.328,310.906ZM216.039,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.056,51.902 -27.872,51.263zM584.348,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.055,51.902 -27.872,51.263z" />
+                <path
+                    android:fillColor="#3ddc84"
+                    android:pathData="m596.328,310.906c22.484,-39.699 46.513,-78.699 68.084,-118.812 4.258,-14.364 -17.654,-23.707 -25.102,-10.703 -22.603,38.973 -45.139,77.986 -67.716,116.974 -107.224,-48.772 -235.192,-48.772 -342.415,0 -23.491,-39.715 -45.567,-80.447 -69.895,-119.56 -10.384,-10.813 -29.373,3.556 -21.753,16.461 22.277,38.562 44.614,77.091 66.914,115.64C88.641,372.042 9.442,494.721 -0,625c266.667,0 533.333,0 800,0C790.261,494.557 711.967,372.81 596.328,310.906ZM216.039,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.056,51.902 -27.872,51.263zM584.348,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.055,51.902 -27.872,51.263z"
+                    android:strokeWidth="1.93078" />
+                <group android:name="anim">
+                    <path
+                        android:fillAlpha="0.999"
+                        android:fillColor="#979797"
+                        android:fillType="nonZero"
+                        android:pathData="m-365.59,1182.578c0,0 -11.042,-480.188 85.394,-399.752 60.916,50.809 142.292,-121.476 169.542,-86.281 5.022,-14.293 176.01,202.148 258.772,33.247 19.774,-40.355 81.088,-119.424 144.881,-29.933 100.418,140.869 205.377,-186.568 333.041,-33.27 67.395,80.927 178.475,-43.713 274.164,36.831 78.914,66.424 154.012,6.899 203.266,113.15 45.039,97.158 66.468,366.007 66.468,366.007L1169.938,171.976L-365.59,171.976Z"
+                        android:strokeAlpha="1"
+                        android:strokeColor="#00000000"
+                        android:strokeWidth="12" />
+                </group>
+            </group>
+        </vector>
+    </aapt:attr>
+
+
+    <target android:name="anim">
+        <aapt:attr name="android:animation">
+            <objectAnimator
+                android:duration="1000"
+                android:propertyName="translateY"
+                android:repeatCount="0"
+                android:valueFrom="0"
+                android:valueTo="-800"
+                android:valueType="floatType"
+                android:interpolator="@android:interpolator/accelerate_decelerate" />
+        </aapt:attr>
+    </target>
+    <target android:name="anim">
+        <aapt:attr name="android:animation">
+            <objectAnimator
+                android:duration="500"
+                android:propertyName="translateX"
+                android:repeatCount="-1"
+                android:repeatMode="reverse"
+                android:valueFrom="-300"
+                android:valueTo="300"
+                android:valueType="floatType"
+                android:interpolator="@android:interpolator/accelerate_decelerate" />
+        </aapt:attr>
+    </target>
+</animated-vector>
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/android.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/android.xml
new file mode 100644
index 0000000..a0207f8
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/android.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="72dp"
+    android:height="72dp"
+    android:viewportHeight="800"
+    android:viewportWidth="800">
+    <path
+        android:fillColor="#3ddc84"
+        android:pathData="m596.328,310.906c22.484,-39.699 46.513,-78.699 68.084,-118.812 4.258,-14.364 -17.654,-23.707 -25.102,-10.703 -22.603,38.973 -45.139,77.986 -67.716,116.974 -107.224,-48.772 -235.192,-48.772 -342.415,0 -23.491,-39.715 -45.567,-80.447 -69.895,-119.56 -10.384,-10.813 -29.373,3.556 -21.753,16.461 22.277,38.562 44.614,77.091 66.914,115.64C88.641,372.042 9.442,494.721 -0,625c266.667,0 533.333,0 800,0C790.261,494.557 711.967,372.81 596.328,310.906ZM216.039,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.056,51.902 -27.872,51.263zM584.348,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.055,51.902 -27.872,51.263z"
+        android:strokeWidth="1.93078" />
+</vector>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/foreground.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/foreground.xml
new file mode 100644
index 0000000..c46194a
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/foreground.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/android"
+    android:inset="25%">
+</inset>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/ic_launcher.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 0000000..59296dc
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@color/logo_background" />
+    <item android:drawable="@drawable/foreground" />
+</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/splashscreen_icon.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/splashscreen_icon.xml
new file mode 100644
index 0000000..a8c4e96
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/drawable/splashscreen_icon.xml
@@ -0,0 +1,28 @@
+<!--
+  Copyright 2021 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="800"
+    android:viewportWidth="800">
+
+    <path
+        android:fillColor="#3ddc84"
+        android:pathData="m596.328,310.906c22.484,-39.699 46.513,-78.699 68.084,-118.812 4.258,-14.364 -17.654,-23.707 -25.102,-10.703 -22.603,38.973 -45.139,77.986 -67.716,116.974 -107.224,-48.772 -235.192,-48.772 -342.415,0 -23.491,-39.715 -45.567,-80.447 -69.895,-119.56 -10.384,-10.813 -29.373,3.556 -21.753,16.461 22.277,38.562 44.614,77.091 66.914,115.64C88.641,372.042 9.442,494.721 -0,625c266.667,0 533.333,0 800,0C790.261,494.557 711.967,372.81 596.328,310.906ZM216.039,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.056,51.902 -27.872,51.263zM584.348,511.17c-24.214,1.092 -41.724,-28.29 -29.21,-49.016 10.591,-21.768 44.808,-23.114 57.082,-2.246 14.575,21.206 -2.055,51.902 -27.872,51.263z"
+        android:strokeWidth="2" />
+
+</vector>
+
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/layout/main_activity.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/layout/main_activity.xml
new file mode 100644
index 0000000..e170fbe
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/layout/main_activity.xml
@@ -0,0 +1,115 @@
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="SampleItem" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="SampleItem" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="SampleItem" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="SampleItem" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="SampleItem" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="SampleItem" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="70dp"
+        android:gravity="center"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="SampleItem" />
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/close_app"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:text="Close App">
+    </Button>
+</LinearLayout>
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/colors.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/colors.xml
new file mode 100644
index 0000000..da0c8fc
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <color name="logo_background">#3C3C3C</color>
+    <color name="windowBackground">#CFCFCF</color>
+    <color name="splashScreenBackground">#474747</color>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/dimens.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..c860cda
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <dimen name="content_animation_margin_start">30dp</dimen>
+    <dimen name="content_animation_margin_end">10dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/strings.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ad5d050
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <string name="app_name">AndroidX SplashScreen Sample</string>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/styles.xml b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/styles.xml
new file mode 100644
index 0000000..fafcea9
--- /dev/null
+++ b/core/core-splashscreen/core-splashscreen-sample/src/main/res/values/styles.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<resources>
+
+    <style name="Theme.App" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="android:windowBackground">@color/windowBackground</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+    </style>
+
+    <style name="Theme.App.Starting" parent="Theme.SplashScreen">
+        <item name="windowSplashScreenBackground">@color/splashScreenBackground</item>
+        <item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_icon</item>
+        <item name="windowSplashScreenAnimationDuration">2000</item>
+        <item name="postSplashScreenTheme">@style/Theme.App</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/androidTest/AndroidManifest.xml b/core/core-splashscreen/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..03a85a1
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<!--
+  Copyright 2021 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.core.splashscreen.test">
+
+    <application>
+        <activity
+            android:name=".SplashScreenTestActivity"
+            android:theme="@style/Theme.Test.Starting">
+            <intent-filter>
+                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name=".SplashScreenAppCompatTestActivity"
+            android:theme="@style/Theme.Test.Starting.AppCompat">
+            <intent-filter>
+                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
new file mode 100644
index 0000000..b46d910
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 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 androidx.core.splashscreen.test
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+public class SplashScreenTestActivity : Activity(), SplashScreenTestControllerHolder {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        controller = SplashScreenTestController(this)
+        controller.onCreate()
+    }
+
+    override lateinit var controller: SplashScreenTestController
+}
+
+public class SplashScreenAppCompatTestActivity :
+    AppCompatActivity(), SplashScreenTestControllerHolder {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        controller = SplashScreenTestController(this)
+        controller.onCreate()
+    }
+
+    override lateinit var controller: SplashScreenTestController
+}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
new file mode 100644
index 0000000..453da24
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2021 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 androidx.core.splashscreen.test
+
+import android.app.Activity
+import android.graphics.Bitmap
+import android.util.TypedValue
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.splashscreen.R as SR
+import androidx.test.runner.screenshot.Screenshot
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.atomic.AtomicBoolean
+
+internal const val EXTRA_ANIMATION_LISTENER = "AnimationListener"
+internal const val EXTRA_SPLASHSCREEN_WAIT = "splashscreen_wait"
+internal const val EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT = "SplashScreenViewScreenShot"
+
+public interface SplashScreenTestControllerHolder {
+    public var controller: SplashScreenTestController
+}
+
+public class SplashScreenTestController(private val activity: Activity) {
+
+    public var splashScreenViewScreenShot: Bitmap? = null
+    public var splashScreenScreenshot: Bitmap? = null
+    public var splashscreenIconId: Int = 0
+    public var splashscreenBackgroundId: Int = 0
+    public var finalAppTheme: Int = 0
+    public var duration: Int = 0
+    public var exitAnimationListenerLatch: CountDownLatch = CountDownLatch(1)
+    public var drawnLatch: CountDownLatch = CountDownLatch(1)
+    public val isCompatActivity: Boolean
+        get() = activity is AppCompatActivity
+
+    // Wait for at least 3 passes to reduce flakiness
+    public var waitedLatch: CountDownLatch = CountDownLatch(3)
+    public val waitBarrier: AtomicBoolean = AtomicBoolean(true)
+    public var hasDrawn: Boolean = false
+
+    public fun onCreate() {
+        val intent = activity.intent
+        val theme = activity.theme
+
+        val useListener = intent.extras?.getBoolean(EXTRA_ANIMATION_LISTENER) ?: false
+        val takeScreenShot =
+            intent.extras?.getBoolean(EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT) ?: false
+        val waitForSplashscreen = intent.extras?.getBoolean(EXTRA_SPLASHSCREEN_WAIT) ?: false
+
+        val tv = TypedValue()
+        theme.resolveAttribute(SR.attr.windowSplashScreenAnimatedIcon, tv, true)
+        splashscreenIconId = tv.resourceId
+
+        theme.resolveAttribute(SR.attr.windowSplashScreenBackground, tv, true)
+        splashscreenBackgroundId = tv.resourceId
+
+        theme.resolveAttribute(SR.attr.postSplashScreenTheme, tv, true)
+        finalAppTheme = tv.resourceId
+
+        theme.resolveAttribute(SR.attr.windowSplashScreenAnimationDuration, tv, true)
+        duration = tv.data
+
+        val splashScreen = activity.installSplashScreen()
+        activity.setContentView(R.layout.main_activity)
+
+        if (waitForSplashscreen) {
+            splashScreen.setKeepVisibleCondition {
+                waitedLatch.countDown()
+                val shouldWait = waitBarrier.get() || waitedLatch.count > 0L
+                if (!shouldWait && takeScreenShot && splashScreenScreenshot == null) {
+                    splashScreenScreenshot = Screenshot.capture().bitmap
+                }
+                shouldWait
+            }
+        }
+
+        if (useListener) {
+            splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
+                if (takeScreenShot) {
+                    splashScreenViewScreenShot = Screenshot.capture().bitmap
+                }
+                exitAnimationListenerLatch.countDown()
+                splashScreenViewProvider.remove()
+            }
+        }
+
+        val view = activity.findViewById<TestView>(R.id.container)
+        view.doOnDraw = { drawnLatch.countDown() }
+    }
+}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenTest.kt
new file mode 100644
index 0000000..f5b5e4e
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenTest.kt
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2021 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 androidx.core.splashscreen.test
+
+import android.app.Instrumentation
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.Bundle
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.screenshot.matchers.MSSIMMatcher
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import org.hamcrest.core.IsNull.notNullValue
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThat
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+import kotlin.reflect.KClass
+
+private const val SPLASH_SCREEN_STYLE_ICON = 1
+private const val KEY_SPLASH_SCREEN_STYLE: String = "android.activity.splashScreenStyle"
+private const val BASIC_SAMPLE_PACKAGE: String = "androidx.core.splashscreen.test"
+private const val LAUNCH_TIMEOUT: Long = 5000
+
+@LargeTest
+@RunWith(Parameterized::class)
+public class SplashscreenTest(
+    public val name: String,
+    public val activityClass: KClass<out SplashScreenTestControllerHolder>
+) {
+
+    private lateinit var device: UiDevice
+
+    public companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        public fun data(): Iterable<Array<Any>> {
+            return listOf(
+                arrayOf("Platform", SplashScreenTestActivity::class),
+                arrayOf("AppCompat", SplashScreenAppCompatTestActivity::class)
+            )
+        }
+    }
+
+    @Before
+    public fun setUp() {
+        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+    }
+
+    @Test
+    public fun compatAttributePopulated() {
+        val activity = startActivityWithSplashScreen()
+        assertEquals(1234, activity.duration)
+        assertEquals(R.color.bg_launcher, activity.splashscreenBackgroundId)
+        val expectedTheme =
+            if (activity.isCompatActivity) R.style.Theme_Test_AppCompat else R.style.Theme_Test
+        assertEquals(expectedTheme, activity.finalAppTheme)
+        assertEquals(R.drawable.android, activity.splashscreenIconId)
+    }
+
+    @Test
+    public fun exitAnimationListenerCalled() {
+        val activity = startActivityWithSplashScreen {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_ANIMATION_LISTENER, true)
+        }
+        assertTrue(activity.exitAnimationListenerLatch.await(2, TimeUnit.SECONDS))
+    }
+
+    @Test
+    public fun splashScreenWaited() {
+        val activity = startActivityWithSplashScreen {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
+        }
+        assertTrue(
+            "Waiting condition was never checked",
+            activity.waitedLatch.await(2, TimeUnit.SECONDS)
+        )
+        assertFalse(
+            "Activity should not have been drawn", activity.hasDrawn
+        )
+        activity.waitBarrier.set(false)
+        assertTrue(
+            "Activity was never drawn",
+            activity.drawnLatch.await(2, TimeUnit.SECONDS)
+        )
+    }
+
+    @Test
+    public fun exitAnimationListenerCalledAfterWait() {
+        val activity = startActivityWithSplashScreen {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
+            it.putExtra(EXTRA_ANIMATION_LISTENER, true)
+        }
+        activity.waitBarrier.set(false)
+        assertTrue(
+            "Activity was never drawn",
+            activity.drawnLatch.await(2, TimeUnit.SECONDS)
+        )
+        assertTrue(activity.exitAnimationListenerLatch.await(2, TimeUnit.SECONDS))
+    }
+
+    @Test
+    public fun splashscreenViewScreenshotComparison() {
+        val activity = startActivityWithSplashScreen {
+            // Clear out any previous instances
+            it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+            it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
+            it.putExtra(EXTRA_ANIMATION_LISTENER, true)
+            it.putExtra(EXTRA_SPLASHSCREEN_VIEW_SCREENSHOT, true)
+        }
+        assertTrue(activity.waitedLatch.await(2, TimeUnit.SECONDS))
+        activity.waitBarrier.set(false)
+        activity.exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
+
+        compareBitmaps(activity.splashScreenScreenshot!!, activity.splashScreenViewScreenShot!!)
+    }
+
+    private fun compareBitmaps(
+        beforeScreenshot: Bitmap,
+        afterScreenshot: Bitmap
+    ) {
+        val beforeBuffer = IntArray(beforeScreenshot.width * beforeScreenshot.height)
+        beforeScreenshot.getPixels(
+            beforeBuffer, 0, beforeScreenshot.width, 0, 0,
+            beforeScreenshot.width, beforeScreenshot.height
+        )
+
+        val afterBuffer = IntArray(afterScreenshot.width * afterScreenshot.height)
+        afterScreenshot.getPixels(
+            afterBuffer, 0, afterScreenshot.width, 0, 0,
+            afterScreenshot.width, afterScreenshot.height
+        )
+
+        val matcher = MSSIMMatcher(0.90).compareBitmaps(
+            beforeBuffer, afterBuffer, afterScreenshot.width,
+            afterScreenshot.height
+        )
+
+        if (!matcher.matches) {
+            val bundle = Bundle()
+            val diff = matcher.diff?.writeToDevice("diff.png")
+            bundle.putString("splashscreen_diff", diff?.absolutePath)
+            bundle.putString(
+                "splashscreen_before",
+                beforeScreenshot.writeToDevice("before.png").absolutePath
+            )
+            bundle.putString(
+                "splashscreen_after",
+                afterScreenshot.writeToDevice("after.png").absolutePath
+            )
+            val path = diff?.parentFile?.path
+            InstrumentationRegistry.getInstrumentation().sendStatus(2, bundle)
+            fail(
+                "SplashScreenView and SplashScreen don't match\n${matcher.comparisonStatistics}" +
+                    "\nResult saved at $path"
+            )
+        }
+    }
+
+    private fun Bitmap.writeToDevice(name: String): File {
+        return writeToDevice(
+            {
+                compress(Bitmap.CompressFormat.PNG, 0 /*ignored for png*/, it)
+            },
+            name
+        )
+    }
+
+    private fun writeToDevice(
+        writeAction: (FileOutputStream) -> Unit,
+        name: String
+    ): File {
+        val deviceOutputDirectory = File(
+            InstrumentationRegistry.getInstrumentation().context.externalCacheDir,
+            "splashscreen_test"
+        )
+        if (!deviceOutputDirectory.exists() && !deviceOutputDirectory.mkdir()) {
+            throw IOException("Could not create folder.")
+        }
+
+        val file = File(deviceOutputDirectory, name)
+        try {
+            FileOutputStream(file).use {
+                writeAction(it)
+            }
+        } catch (e: Exception) {
+            throw IOException(
+                "Could not write file to storage (path: ${file.absolutePath}). " +
+                    " Stacktrace: " + e.stackTrace
+            )
+        }
+        return file
+    }
+
+    private fun startActivityWithSplashScreen(
+        intentModifier: ((Intent) -> Unit)? = null
+    ): SplashScreenTestController {
+        // Start from the home screen
+        device.pressHome()
+
+        // Wait for launcher
+        val launcherPackage: String = device.launcherPackageName
+        assertThat(launcherPackage, notNullValue())
+        device.wait(
+            Until.hasObject(By.pkg(launcherPackage).depth(0)),
+            LAUNCH_TIMEOUT
+        )
+
+        // Launch the app
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val baseIntent = context.packageManager.getLaunchIntentForPackage(
+            BASIC_SAMPLE_PACKAGE
+        )
+        val intent = Intent(baseIntent).apply {
+            component = ComponentName(BASIC_SAMPLE_PACKAGE, activityClass.qualifiedName!!)
+            intentModifier?.invoke(this)
+        }
+
+        val monitor = object : Instrumentation.ActivityMonitor(
+            activityClass.qualifiedName!!,
+            Instrumentation.ActivityResult(0, Intent()), false
+        ) {
+            override fun onStartActivity(intent: Intent?): Instrumentation.ActivityResult? {
+                return if (intent?.component?.packageName == BASIC_SAMPLE_PACKAGE) {
+                    Instrumentation.ActivityResult(0, Intent())
+                } else {
+                    null
+                }
+            }
+        }
+        InstrumentationRegistry.getInstrumentation().addMonitor(monitor)
+
+        context.startActivity(
+            intent,
+            // Force the splash screen to be shown with an icon
+            Bundle().apply { putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_ICON) }
+        )
+        assertTrue(
+            device.wait(
+                Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
+                LAUNCH_TIMEOUT
+            )
+        )
+        val splashScreenTestActivity =
+            monitor.waitForActivityWithTimeout(LAUNCH_TIMEOUT) as SplashScreenTestControllerHolder?
+        if (splashScreenTestActivity == null) {
+            fail(
+                activityClass.simpleName!! + " was not launched after " +
+                    "$LAUNCH_TIMEOUT ms"
+            )
+        }
+        return splashScreenTestActivity!!.controller
+    }
+}
\ No newline at end of file
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/TestView.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/TestView.kt
new file mode 100644
index 0000000..6bc6359
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/TestView.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 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 androidx.core.splashscreen.test
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+public class TestView(
+    context: Context,
+    attrs: AttributeSet
+) : FrameLayout(context, attrs) {
+
+    public var doOnDraw: (() -> Unit) = {}
+
+    override fun onDraw(canvas: Canvas?) {
+        super.onDraw(canvas)
+        doOnDraw()
+    }
+}
\ No newline at end of file
diff --git a/core/core-splashscreen/src/androidTest/res/drawable/android.xml b/core/core-splashscreen/src/androidTest/res/drawable/android.xml
new file mode 100644
index 0000000..822d8e7
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/res/drawable/android.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="72dp"
+    android:height="72dp"
+    android:viewportHeight="100"
+    android:viewportWidth="100">
+    <path
+        android:fillColor="#3ddc84"
+        android:pathData="m0 0 L 0 100 100 100 100 0z"
+        android:strokeWidth="1" />
+</vector>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml b/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
new file mode 100644
index 0000000..6570777
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
@@ -0,0 +1,24 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<androidx.core.splashscreen.test.TestView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="#00FF00">
+
+</androidx.core.splashscreen.test.TestView>
diff --git a/core/core-splashscreen/src/androidTest/res/values/colors.xml b/core/core-splashscreen/src/androidTest/res/values/colors.xml
new file mode 100644
index 0000000..306e151
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+  <color name="bg_launcher">#15FFFF</color>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/androidTest/res/values/styles.xml b/core/core-splashscreen/src/androidTest/res/values/styles.xml
new file mode 100644
index 0000000..743d243
--- /dev/null
+++ b/core/core-splashscreen/src/androidTest/res/values/styles.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+
+    <style name="Theme.Test" parent="android:Theme.Material.Light.NoActionBar">
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+        <item name="android:fitsSystemWindows">false</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+    </style>
+
+    <style name="Theme.Test.Starting" parent="Theme.SplashScreen">
+        <item name="windowSplashScreenBackground">@color/bg_launcher</item>
+        <item name="windowSplashScreenAnimatedIcon">@drawable/android</item>
+        <item name="windowSplashScreenAnimationDuration">1234</item>
+        <item name="postSplashScreenTheme">@style/Theme.Test</item>
+    </style>
+
+    <!-- Themes for AppCompat Tests -->
+    <style name="Theme.Test.AppCompat" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+        <item name="android:fitsSystemWindows">false</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+    </style>
+
+    <style name="Theme.Test.Starting.AppCompat" parent="Theme.SplashScreen">
+        <item name="windowSplashScreenBackground">@color/bg_launcher</item>
+        <item name="windowSplashScreenAnimatedIcon">@drawable/android</item>
+        <item name="windowSplashScreenAnimationDuration">1234</item>
+        <item name="postSplashScreenTheme">@style/Theme.Test.AppCompat</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/AndroidManifest.xml b/core/core-splashscreen/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..284f45e
--- /dev/null
+++ b/core/core-splashscreen/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.core.splashscreen">
+
+    <application>
+        <activity android:name=".test.SplashScreenAppCompatTestActivity" />
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
new file mode 100644
index 0000000..94c0b67
--- /dev/null
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
@@ -0,0 +1,343 @@
+/*
+ * Copyright 2021 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 androidx.core.splashscreen
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.res.Resources
+import android.os.Build.VERSION.PREVIEW_SDK_INT
+import android.os.Build.VERSION.SDK_INT
+import android.util.TypedValue
+import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.widget.ImageView
+import androidx.annotation.MainThread
+import androidx.annotation.RequiresApi
+import androidx.core.splashscreen.SplashScreen.KeepOnScreenCondition
+
+/**
+ * Compatibly class for the SplashScreen API introduced in API 31.
+ *
+ * On API 31+ (Android 12+) this class calls the platform methods.
+ *
+ * Prior API 31, the platform behavior is replicated with the exception of the Animated Vector
+ * Drawable support on the launch screen.
+ *
+ * To use this class, the theme of the starting Activity needs set [R.style.Theme_SplashScreen] as
+ * its parent and the [R.attr.windowSplashScreenAnimatedIcon] and [R.attr.postSplashScreenTheme]`
+ * attribute need to be set.
+ */
+@SuppressLint("CustomSplashScreen")
+public class SplashScreen private constructor(activity: Activity) {
+
+    @SuppressLint("NewApi") // TODO(188897399) Remove once "S" is finalized
+    private val impl = when {
+        SDK_INT >= 31 -> Impl31(activity)
+        SDK_INT == 30 && PREVIEW_SDK_INT > 0 -> Impl31(activity)
+        SDK_INT >= 23 -> Impl23(activity)
+        else -> Impl(activity)
+    }
+
+    public companion object {
+
+        /**
+         * Creates a [SplashScreen] instance associated with this [Activity] and handles
+         * setting the theme to [R.attr.postSplashScreenTheme].
+         *
+         * This needs to be called before [Activity.setContentView] or other view operation on
+         * the root view (e.g setting flags).
+         *
+         * Alternatively, if a [SplashScreen] instance is not required, the them can manually be
+         * set using [Activity.setTheme].
+         */
+        @JvmStatic
+        public fun Activity.installSplashScreen(): SplashScreen {
+            val splashScreen = SplashScreen(this)
+            splashScreen.install()
+            return splashScreen
+        }
+    }
+
+    /**
+     * Sets the condition to keep the splash screen visible.
+     *
+     * The splash will stay visible until the condition isn't met anymore.
+     * The condition is evaluated before each request to draw the application, so it needs to be
+     * fast to avoid blocking the UI.
+     *
+     * @param condition The condition evaluated to decide whether to keep the splash screen on
+     * screen
+     */
+    public fun setKeepVisibleCondition(condition: KeepOnScreenCondition) {
+        impl.setKeepVisibleCondition(condition)
+    }
+
+    /**
+     * Sets a listener that will be called when the splashscreen is ready to be removed.
+     *
+     * If a listener is set, the splashscreen won't be automatically removed and the application
+     * needs to manually call [SplashScreenViewProvider.remove].
+     *
+     * IF no listener is set, the splashscreen will be automatically removed once the app is
+     * ready to draw.
+     *
+     * The listener will be called on the ui thread.
+     *
+     * @param listener The [OnExitAnimationListener] that will be called when the splash screen
+     * is ready to be dismissed.
+     *
+     * @see setKeepVisibleCondition
+     * @see OnExitAnimationListener
+     * @see SplashScreenViewProvider
+     */
+    @SuppressWarnings("ExecutorRegistration") // Always runs on the MainThread
+    public fun setOnExitAnimationListener(listener: OnExitAnimationListener) {
+        impl.setOnExitAnimationListener(listener)
+    }
+
+    private fun install() {
+        impl.install()
+    }
+
+    /**
+     * Listener to be passed in [SplashScreen.setOnExitAnimationListener].
+     *
+     * The listener will be called once the splash screen is ready to be removed and provides a
+     * reference to a [SplashScreenViewProvider] that can be used to customize the exit
+     * animation of the splash screen.
+     */
+    public fun interface OnExitAnimationListener {
+
+        /**
+         * Callback called when the splash screen is ready to be dismissed. The caller is
+         * responsible for animating and removing splash screen using the provided
+         * [splashScreenViewProvider].
+         *
+         * The caller **must** call [SplashScreenViewProvider.remove] once it's done with the
+         * splash screen.
+         *
+         * @param splashScreenViewProvider An object holding a reference to the displayed splash
+         * screen.
+         */
+        @MainThread
+        public fun onSplashScreenExit(splashScreenViewProvider: SplashScreenViewProvider)
+    }
+
+    /**
+     * Condition evaluated to check if the splash screen should remain on screen
+     *
+     * The splash screen will stay visible until the condition isn't met anymore.
+     * The condition is evaluated before each request to draw the application, so it needs to be
+     * fast to avoid blocking the UI.
+     */
+    public fun interface KeepOnScreenCondition {
+
+        /**
+         * Callback evaluated before every requests to draw the Activity. If it returns `true`, the
+         * splash screen will be kept visible to hide the Activity below.
+         *
+         * This callback is evaluated in the main thread.
+         */
+        @MainThread
+        public fun shouldKeepOnScreen(): Boolean
+    }
+
+    private open class Impl(val activity: Activity) {
+        var finalThemeId: Int = 0
+        var backgroundResId: Int? = null
+        var backgroundColor: Int? = null
+        var icon: Int = 0
+
+        var splashScreenWaitPredicate = KeepOnScreenCondition { false }
+        private var animationListener: OnExitAnimationListener? = null
+        private var mSplashScreenViewProvider: SplashScreenViewProvider? = null
+
+        open fun install() {
+            val typedValue = TypedValue()
+            val currentTheme = activity.theme
+            if (currentTheme.resolveAttribute(
+                    R.attr.windowSplashScreenBackground,
+                    typedValue,
+                    true
+                )
+            ) {
+                backgroundResId = typedValue.resourceId
+                backgroundColor = typedValue.data
+            }
+            if (currentTheme.resolveAttribute(
+                    R.attr.windowSplashScreenAnimatedIcon,
+                    typedValue,
+                    true
+                )
+            ) {
+                icon = typedValue.resourceId
+            }
+            setPostSplashScreenTheme(currentTheme, typedValue)
+        }
+
+        protected fun setPostSplashScreenTheme(
+            currentTheme: Resources.Theme,
+            typedValue: TypedValue
+        ) {
+            if (currentTheme.resolveAttribute(R.attr.postSplashScreenTheme, typedValue, true)) {
+                finalThemeId = typedValue.resourceId
+                if (finalThemeId != 0) {
+                    activity.setTheme(finalThemeId)
+                }
+            } else {
+                throw Resources.NotFoundException(
+                    "Cannot set AppTheme. No theme value defined for attribute " +
+                        activity.resources.getResourceName(R.attr.postSplashScreenTheme)
+                )
+            }
+        }
+
+        open fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
+            splashScreenWaitPredicate = keepOnScreenCondition
+            val contentView = activity.findViewById<View>(android.R.id.content)
+            val observer = contentView.viewTreeObserver
+            observer.addOnPreDrawListener(object : OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
+                        return false
+                    }
+                    contentView.viewTreeObserver.removeOnPreDrawListener(this)
+                    mSplashScreenViewProvider?.let(::dispatchOnExitAnimation)
+                    return true
+                }
+            })
+        }
+
+        open fun setOnExitAnimationListener(exitAnimationListener: OnExitAnimationListener) {
+            animationListener = exitAnimationListener
+
+            val splashScreenViewProvider = SplashScreenViewProvider(activity)
+            val finalBackgroundResId = backgroundResId
+            val finalBackgroundColor = backgroundColor
+            if (finalBackgroundResId != null && finalBackgroundResId != Resources.ID_NULL) {
+                splashScreenViewProvider.view.setBackgroundResource(finalBackgroundResId)
+            } else if (finalBackgroundColor != null) {
+                splashScreenViewProvider.view.setBackgroundColor(finalBackgroundColor)
+            } else {
+                splashScreenViewProvider.view.background = activity.window.decorView.background
+            }
+
+            splashScreenViewProvider.view.findViewById<ImageView>(R.id.splashscreen_icon_view)
+                .setBackgroundResource(icon)
+
+            splashScreenViewProvider.view.addOnLayoutChangeListener(
+                object : OnLayoutChangeListener {
+                    override fun onLayoutChange(
+                        view: View,
+                        left: Int,
+                        top: Int,
+                        right: Int,
+                        bottom: Int,
+                        oldLeft: Int,
+                        oldTop: Int,
+                        oldRight: Int,
+                        oldBottom: Int
+                    ) {
+                        adjustInsets(view, splashScreenViewProvider)
+                        if (!view.isAttachedToWindow) {
+                            return
+                        }
+
+                        view.removeOnLayoutChangeListener(this)
+                        if (!splashScreenWaitPredicate.shouldKeepOnScreen()) {
+                            dispatchOnExitAnimation(splashScreenViewProvider)
+                        } else {
+                            mSplashScreenViewProvider = splashScreenViewProvider
+                        }
+                    }
+                })
+        }
+
+        fun dispatchOnExitAnimation(splashScreenViewProvider: SplashScreenViewProvider) {
+            val finalListener = animationListener ?: return
+            animationListener = null
+            splashScreenViewProvider.view.postOnAnimation {
+                finalListener.onSplashScreenExit(splashScreenViewProvider)
+            }
+        }
+
+        /**
+         * Adjust the insets to avoid any jump between the actual splash screen and the
+         * SplashScreen View
+         */
+        open fun adjustInsets(
+            view: View,
+            splashScreenViewProvider: SplashScreenViewProvider
+        ) {
+            // No-op
+        }
+    }
+
+    @Suppress("DEPRECATION")
+    @RequiresApi(23)
+    private class Impl23(activity: Activity) : Impl(activity) {
+        override fun adjustInsets(
+            view: View,
+            splashScreenViewProvider: SplashScreenViewProvider
+        ) {
+            // Offset the icon if the insets have changed
+            val rootWindowInsets = view.rootWindowInsets
+            val ty =
+                rootWindowInsets.systemWindowInsetTop - rootWindowInsets.systemWindowInsetBottom
+            splashScreenViewProvider.iconView.translationY = -ty.toFloat() / 2f
+        }
+    }
+
+    @RequiresApi(31) // TODO(188897399) Update to "S" once finalized
+    private class Impl31(activity: Activity) : Impl(activity) {
+        var preDrawListener: OnPreDrawListener? = null
+
+        override fun install() {
+            setPostSplashScreenTheme(activity.theme, TypedValue())
+        }
+
+        override fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
+            splashScreenWaitPredicate = keepOnScreenCondition
+            val contentView = activity.findViewById<View>(android.R.id.content)
+            val observer = contentView.viewTreeObserver
+
+            if (preDrawListener != null && observer.isAlive) {
+                observer.removeOnPreDrawListener(preDrawListener)
+            }
+            preDrawListener = object : OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
+                        return false
+                    }
+                    contentView.viewTreeObserver.removeOnPreDrawListener(this)
+                    return true
+                }
+            }
+            observer.addOnPreDrawListener(preDrawListener)
+        }
+
+        override fun setOnExitAnimationListener(
+            exitAnimationListener: OnExitAnimationListener
+        ) {
+            activity.splashScreen.setOnExitAnimationListener {
+                val splashScreenViewProvider = SplashScreenViewProvider(it, activity)
+                exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
new file mode 100644
index 0000000..11eda6d
--- /dev/null
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 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 androidx.core.splashscreen
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.window.SplashScreenView
+import androidx.annotation.RequiresApi
+
+/**
+ * Contains a copy of the splash screen used to create a custom animation from the splash screen
+ * to the application.
+ *
+ * The splashscreen is accessible using [SplashScreenViewProvider.view] and the view
+ * containing the icon using [SplashScreenViewProvider.iconView].
+ *
+ * This class also contains time information about the animated icon (for API 31+).
+ *
+ * The application always needs to call [SplashScreenViewProvider.remove] once it's done
+ * with it.
+ */
+@SuppressLint("ViewConstructor")
+public class SplashScreenViewProvider internal constructor(ctx: Activity) {
+
+    @RequiresApi(31)
+    internal constructor(platformView: SplashScreenView, ctx: Activity) : this(ctx) {
+        (impl as ViewImpl31).platformView = platformView
+    }
+
+    @SuppressLint("NewApi") // TODO(188897399) Remove once "S" is finalized
+    private val impl: ViewImpl = when {
+        Build.VERSION.SDK_INT >= 31 -> ViewImpl31(ctx)
+        Build.VERSION.SDK_INT == 30 && Build.VERSION.PREVIEW_SDK_INT > 0 -> ViewImpl31(ctx)
+        else -> ViewImpl(ctx)
+    }
+
+    /**
+     * The splash screen view, copied into this application process.
+     *
+     * This view can be used to create custom animation from the splash screen to the application
+     */
+    public val view: View get() = impl.splashScreenView
+
+    /**
+     * The view containing the splashscreen icon as defined by
+     * [R.attr.windowSplashScreenAnimatedIcon]
+     */
+    public val iconView: View get() = impl.iconView
+
+    /**
+     * Start time of the icon animation.
+     *
+     * On API 31+, returns the number of millisecond since the Epoch time (1970-1-1T00:00:00Z)
+     *
+     * Below API 31, returns 0 because the icon cannot be animated.
+     */
+    public val iconAnimationStartMillis: Long get() = impl.iconAnimationStartMillis
+
+    /**
+     * Duration of the icon animation as provided in [R.attr.
+     */
+    public val iconAnimationDurationMillis: Long get() = impl.iconAnimationDurationMillis
+
+    /**
+     * Remove the SplashScreen's view from the view hierarchy.
+     *
+     * This always needs to be called when an
+     * [androidx.core.splashscreen.SplashScreen.OnExitAnimationListener]
+     * is set.
+     */
+    public fun remove(): Unit = impl.remove()
+
+    private open class ViewImpl(val activity: Activity) {
+
+        private val _splashScreenView: ViewGroup by lazy {
+            FrameLayout.inflate(
+                activity,
+                R.layout.splash_screen_view,
+                null
+            ) as ViewGroup
+        }
+
+        init {
+            val content = activity.findViewById<ViewGroup>(android.R.id.content)
+            content.addView(_splashScreenView)
+        }
+
+        open val splashScreenView: ViewGroup get() = _splashScreenView
+        open val iconView: View get() = splashScreenView.findViewById(R.id.splashscreen_icon_view)
+        open val iconAnimationStartMillis: Long get() = 0
+        open val iconAnimationDurationMillis: Long get() = 0
+        open fun remove() =
+            activity.findViewById<ViewGroup>(android.R.id.content).removeView(splashScreenView)
+    }
+
+    @RequiresApi(31)
+    private class ViewImpl31(activity: Activity) : ViewImpl(activity) {
+        lateinit var platformView: SplashScreenView
+
+        override val splashScreenView get() = platformView
+
+        override val iconView get() = platformView.iconView!!
+
+        override val iconAnimationStartMillis: Long
+            get() = platformView.iconAnimationStart?.toEpochMilli() ?: 0
+
+        override val iconAnimationDurationMillis: Long
+            get() = platformView.iconAnimationDuration?.toMillis() ?: 0
+
+        override fun remove() = platformView.remove()
+    }
+}
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/package-info.java b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/package-info.java
new file mode 100644
index 0000000..904934b
--- /dev/null
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/package-info.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 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.
+ */
+
+/**
+ * This Splash Screen library provides compatibility support for the
+ * <code>android.window.SplashScreen</code> APIs down to API 21, with support of the splash
+ * screen icon from API 23.
+ * <p>
+ * It is composed of a compatibility theme
+ * {@link androidx.core.splashscreen.R.style#Theme_SplashScreen}
+ * that needs to be set as the starting theme of the activity and a programmatic API in
+ * {@link androidx.core.splashscreen.SplashScreen}.
+ * <p>
+ * To use it, the theme of the launching Activity must inherit from
+ * <code>Theme.SplashScreen</code>
+ * <p>
+ * <i>AndroidManifest.xml:</i>
+ * <pre class="prettyprint">
+ *     &lt;manifest...>
+ *         &lt;application>
+ *         &lt;activity>
+ *              android:name=".MainActivity"
+ *              android:theme="@style/Theme.App.Starting"/&gt;
+ *      &lt;/manifest>
+ * </pre>
+ * <i>res/values/styles.xml:</i>
+ * <pre class="prettyprint">
+ * &lt;resources>
+ *     &lt;style name="Theme.App" parent="...">
+ *     ...
+ *     &lt;/style>
+ *
+ *    &lt;style name="Theme.App.Starting" parent="Theme.SplashScreen">
+ *        &lt;item name="windowSplashScreenBackground">@color/splashScreenBackground&lt;/item>
+ *        &lt;item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_icon&lt;/item>
+ *        &lt;item name="windowSplashScreenAnimationDuration">2000&lt;/item>
+ *        &lt;item name="postSplashScreenTheme">@style/Theme.App&lt;/item>
+ * &lt;/resources>
+ * </pre>
+ *
+ * <i>MainActivity.java:</i>
+ * <pre class="prettyprint">
+ *     class MainActivity : Activity {
+ *         fun onCreate() {
+ *             super.onCreate()
+ *             val splashScreen = installSplashScreen()
+ *
+ *             // Set the content view right after installing the splash screen
+ *             setContentView(R.layout.main_activity)
+ *         }
+ *     }
+ * </pre>
+ */
+package androidx.core.splashscreen;
diff --git a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml b/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml
new file mode 100644
index 0000000..30562b0
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:gravity="fill">
+        <color android:color="?attr/windowSplashScreenBackground" />
+    </item>
+    <item
+        android:drawable="?attr/windowSplashScreenAnimatedIcon"
+        android:gravity="center"
+        android:width="@dimen/splashscreen_icon_size"
+        android:height="@dimen/splashscreen_icon_size" />
+</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
new file mode 100644
index 0000000..28bd4d5
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<color xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?attr/windowSplashScreenBackground" />
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/layout/splash_screen_view.xml b/core/core-splashscreen/src/main/res/layout/splash_screen_view.xml
new file mode 100644
index 0000000..44ab5e19
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/layout/splash_screen_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+  <ImageView
+      android:id="@+id/splashscreen_icon_view"
+      android:layout_width="@dimen/splashscreen_icon_size"
+      android:layout_height="@dimen/splashscreen_icon_size"
+      android:layout_gravity="center" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/values-v27/styles.xml b/core/core-splashscreen/src/main/res/values-v27/styles.xml
new file mode 100644
index 0000000..caa29a6
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values-v27/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+
+    <style name="Theme.SplashScreen" parent="Theme.SplashScreenBase">
+        <item name="android:windowLayoutInDisplayCutoutMode">default</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/values-v29/styles.xml b/core/core-splashscreen/src/main/res/values-v29/styles.xml
new file mode 100644
index 0000000..dfc370f
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values-v29/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+
+    <style name="Theme.SplashScreen" parent="Theme.SplashScreenBase">
+        <item name="android:enforceStatusBarContrast">false</item>
+        <item name="android:enforceNavigationBarContrast">false</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/values-v31/styles.xml b/core/core-splashscreen/src/main/res/values-v31/styles.xml
new file mode 100644
index 0000000..7f04b15
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values-v31/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+
+    <style name="Theme.SplashScreen" parent="android:Theme.DeviceDefault.NoActionBar">
+        <item name="android:windowSplashScreenAnimatedIcon">?windowSplashScreenAnimatedIcon</item>
+        <item name="android:windowSplashScreenBackground">?windowSplashScreenBackground</item>
+        <item name="android:windowSplashScreenAnimationDuration">
+            ?windowSplashScreenAnimationDuration
+        </item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/values/attrs.xml b/core/core-splashscreen/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..5bbfaa3
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values/attrs.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+
+    <!-- Icon to set for the splashscreen. <p>
+     On API31+, this can be an animated icon by overriding it in the [res/drawable-v31/]
+     directory Defaults to @drawable/ic_launcher. -->
+    <attr name="windowSplashScreenAnimatedIcon" format="reference" />
+
+    <!-- Background color of the splash screen. Defaults to the theme's windowBackground-->
+    <attr name="windowSplashScreenBackground" format="color" />
+
+    <!-- Duration of the Animated Icon Animation -->
+    <attr name="windowSplashScreenAnimationDuration" format="integer" />
+
+    <!-- Theme to apply to the Activity once the splash screen is dismissed-->
+    <attr name="postSplashScreenTheme" format="reference" />
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/values/dimens.xml b/core/core-splashscreen/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..7c5b998
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <dimen name="splashscreen_icon_size">160dp</dimen>
+    <integer name="default_icon_animation_duration">10000</integer>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/values/public.xml b/core/core-splashscreen/src/main/res/values/public.xml
new file mode 100644
index 0000000..f90eec9
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values/public.xml
@@ -0,0 +1,22 @@
+<!--
+  Copyright 2021 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.
+  -->
+<resources>
+    <public name="windowSplashScreenAnimatedIcon" type="attr" />
+    <public name="windowSplashScreenBackground" type="attr" />
+    <public name="windowSplashScreenAnimationDuration" type="attr" />
+    <public name="postSplashScreenTheme" type="attr" />
+    <public name="Theme.SplashScreen" type="style" />
+</resources>
diff --git a/core/core-splashscreen/src/main/res/values/styles.xml b/core/core-splashscreen/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ec205a7
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values/styles.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+
+    <style name="Theme.SplashScreenBase" parent="android:Theme.NoTitleBar">
+        <item name="android:windowBackground">@drawable/compat_splash_screen</item>
+        <item name="android:opacity">opaque</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+        <item name="android:fitsSystemWindows">false</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+    </style>
+
+    <style name="Theme.SplashScreen" parent="Theme.SplashScreenBase">
+        <item name="postSplashScreenTheme">?android:attr/theme</item>
+        <item name="windowSplashScreenAnimationDuration">
+            @integer/default_icon_animation_duration
+        </item>
+        <item name="windowSplashScreenBackground">@android:color/background_light</item>
+        <item name="windowSplashScreenAnimatedIcon">@android:drawable/sym_def_app_icon</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 3b15333..829f174 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -30,6 +30,7 @@
     method public static void finishAfterTransition(android.app.Activity);
     method public static android.net.Uri? getReferrer(android.app.Activity);
     method @Deprecated public static boolean invalidateOptionsMenu(android.app.Activity!);
+    method public static boolean isLaunchedFromBubble(android.app.Activity);
     method public static void postponeEnterTransition(android.app.Activity);
     method public static void recreate(android.app.Activity);
     method public static androidx.core.view.DragAndDropPermissionsCompat? requestDragAndDropPermissions(android.app.Activity!, android.view.DragEvent!);
@@ -343,6 +344,9 @@
     field public static final int FLAG_ONGOING_EVENT = 2; // 0x2
     field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8
     field public static final int FLAG_SHOW_LIGHTS = 1; // 0x1
+    field public static final int FOREGROUND_SERVICE_DEFAULT = 0; // 0x0
+    field public static final int FOREGROUND_SERVICE_DEFERRED = 2; // 0x2
+    field public static final int FOREGROUND_SERVICE_IMMEDIATE = 1; // 0x1
     field public static final int GROUP_ALERT_ALL = 0; // 0x0
     field public static final int GROUP_ALERT_CHILDREN = 2; // 0x2
     field public static final int GROUP_ALERT_SUMMARY = 1; // 0x1
@@ -516,6 +520,7 @@
     method public androidx.core.app.NotificationCompat.Builder setDefaults(int);
     method public androidx.core.app.NotificationCompat.Builder setDeleteIntent(android.app.PendingIntent?);
     method public androidx.core.app.NotificationCompat.Builder setExtras(android.os.Bundle?);
+    method public androidx.core.app.NotificationCompat.Builder setForegroundServiceBehavior(int);
     method public androidx.core.app.NotificationCompat.Builder setFullScreenIntent(android.app.PendingIntent?, boolean);
     method public androidx.core.app.NotificationCompat.Builder setGroup(String?);
     method public androidx.core.app.NotificationCompat.Builder setGroupAlertBehavior(int);
@@ -1473,16 +1478,52 @@
     method public static void setMock(android.location.Location, boolean);
   }
 
+  public interface LocationListenerCompat extends android.location.LocationListener {
+    method public default void onStatusChanged(String, int, android.os.Bundle?);
+  }
+
   public final class LocationManagerCompat {
     method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void getCurrentLocation(android.location.LocationManager, String, androidx.core.os.CancellationSignal?, java.util.concurrent.Executor, androidx.core.util.Consumer<android.location.Location!>);
     method public static String? getGnssHardwareModelName(android.location.LocationManager);
     method public static int getGnssYearOfHardware(android.location.LocationManager);
+    method public static boolean hasProvider(android.location.LocationManager, String);
     method public static boolean isLocationEnabled(android.location.LocationManager);
     method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public static boolean registerGnssStatusCallback(android.location.LocationManager, androidx.core.location.GnssStatusCompat.Callback, android.os.Handler);
     method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public static boolean registerGnssStatusCallback(android.location.LocationManager, java.util.concurrent.Executor, androidx.core.location.GnssStatusCompat.Callback);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void removeUpdates(android.location.LocationManager, androidx.core.location.LocationListenerCompat);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void requestLocationUpdates(android.location.LocationManager, String, androidx.core.location.LocationRequestCompat, java.util.concurrent.Executor, androidx.core.location.LocationListenerCompat);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void requestLocationUpdates(android.location.LocationManager, String, androidx.core.location.LocationRequestCompat, androidx.core.location.LocationListenerCompat, android.os.Looper);
     method public static void unregisterGnssStatusCallback(android.location.LocationManager, androidx.core.location.GnssStatusCompat.Callback);
   }
 
+  public final class LocationRequestCompat {
+    method @IntRange(from=1) public long getDurationMillis();
+    method @IntRange(from=0) public long getIntervalMillis();
+    method @IntRange(from=0) public long getMaxUpdateDelayMillis();
+    method @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) public int getMaxUpdates();
+    method @FloatRange(from=0, to=java.lang.Float.MAX_VALUE) public float getMinUpdateDistanceMeters();
+    method @IntRange(from=0) public long getMinUpdateIntervalMillis();
+    method public int getQuality();
+    field public static final long PASSIVE_INTERVAL = 9223372036854775807L; // 0x7fffffffffffffffL
+    field public static final int QUALITY_BALANCED_POWER_ACCURACY = 102; // 0x66
+    field public static final int QUALITY_HIGH_ACCURACY = 100; // 0x64
+    field public static final int QUALITY_LOW_POWER = 104; // 0x68
+  }
+
+  public static final class LocationRequestCompat.Builder {
+    ctor public LocationRequestCompat.Builder(long);
+    ctor public LocationRequestCompat.Builder(androidx.core.location.LocationRequestCompat);
+    method public androidx.core.location.LocationRequestCompat build();
+    method public androidx.core.location.LocationRequestCompat.Builder clearMinUpdateIntervalMillis();
+    method public androidx.core.location.LocationRequestCompat.Builder setDurationMillis(@IntRange(from=1) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setIntervalMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setMaxUpdateDelayMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setMaxUpdates(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+    method public androidx.core.location.LocationRequestCompat.Builder setMinUpdateDistanceMeters(@FloatRange(from=0, to=java.lang.Float.MAX_VALUE) float);
+    method public androidx.core.location.LocationRequestCompat.Builder setMinUpdateIntervalMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setQuality(int);
+  }
+
 }
 
 package androidx.core.math {
@@ -1979,11 +2020,16 @@
     method public android.net.Uri? getLinkUri();
     method public int getSource();
     method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+    method @RequiresApi(31) public static android.util.Pair<android.view.ContentInfo!,android.view.ContentInfo!> partition(android.view.ContentInfo, java.util.function.Predicate<android.content.ClipData.Item!>);
+    method @RequiresApi(31) public android.view.ContentInfo toContentInfo();
+    method @RequiresApi(31) public static androidx.core.view.ContentInfoCompat toContentInfoCompat(android.view.ContentInfo);
     field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
     field public static final int SOURCE_APP = 0; // 0x0
+    field public static final int SOURCE_AUTOFILL = 4; // 0x4
     field public static final int SOURCE_CLIPBOARD = 1; // 0x1
     field public static final int SOURCE_DRAG_AND_DROP = 3; // 0x3
     field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+    field public static final int SOURCE_PROCESS_TEXT = 5; // 0x5
   }
 
   public static final class ContentInfoCompat.Builder {
@@ -3409,13 +3455,16 @@
 
   public final class EdgeEffectCompat {
     ctor @Deprecated public EdgeEffectCompat(android.content.Context!);
+    method public static android.widget.EdgeEffect create(android.content.Context, android.util.AttributeSet?);
     method @Deprecated public boolean draw(android.graphics.Canvas!);
     method @Deprecated public void finish();
+    method public static float getDistance(android.widget.EdgeEffect);
     method @Deprecated public boolean isFinished();
     method @Deprecated public boolean onAbsorb(int);
     method @Deprecated public boolean onPull(float);
     method @Deprecated public boolean onPull(float, float);
     method public static void onPull(android.widget.EdgeEffect, float, float);
+    method public static float onPullDistance(android.widget.EdgeEffect, float, float);
     method @Deprecated public boolean onRelease();
     method @Deprecated public void setSize(int, int);
   }
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 807201b..b385db9 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -30,6 +30,7 @@
     method public static void finishAfterTransition(android.app.Activity);
     method public static android.net.Uri? getReferrer(android.app.Activity);
     method @Deprecated public static boolean invalidateOptionsMenu(android.app.Activity!);
+    method public static boolean isLaunchedFromBubble(android.app.Activity);
     method public static void postponeEnterTransition(android.app.Activity);
     method public static void recreate(android.app.Activity);
     method public static androidx.core.view.DragAndDropPermissionsCompat? requestDragAndDropPermissions(android.app.Activity!, android.view.DragEvent!);
@@ -343,6 +344,9 @@
     field public static final int FLAG_ONGOING_EVENT = 2; // 0x2
     field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8
     field public static final int FLAG_SHOW_LIGHTS = 1; // 0x1
+    field public static final int FOREGROUND_SERVICE_DEFAULT = 0; // 0x0
+    field public static final int FOREGROUND_SERVICE_DEFERRED = 2; // 0x2
+    field public static final int FOREGROUND_SERVICE_IMMEDIATE = 1; // 0x1
     field public static final int GROUP_ALERT_ALL = 0; // 0x0
     field public static final int GROUP_ALERT_CHILDREN = 2; // 0x2
     field public static final int GROUP_ALERT_SUMMARY = 1; // 0x1
@@ -516,6 +520,7 @@
     method public androidx.core.app.NotificationCompat.Builder setDefaults(int);
     method public androidx.core.app.NotificationCompat.Builder setDeleteIntent(android.app.PendingIntent?);
     method public androidx.core.app.NotificationCompat.Builder setExtras(android.os.Bundle?);
+    method public androidx.core.app.NotificationCompat.Builder setForegroundServiceBehavior(int);
     method public androidx.core.app.NotificationCompat.Builder setFullScreenIntent(android.app.PendingIntent?, boolean);
     method public androidx.core.app.NotificationCompat.Builder setGroup(String?);
     method public androidx.core.app.NotificationCompat.Builder setGroupAlertBehavior(int);
@@ -1473,16 +1478,52 @@
     method public static void setMock(android.location.Location, boolean);
   }
 
+  public interface LocationListenerCompat extends android.location.LocationListener {
+    method public default void onStatusChanged(String, int, android.os.Bundle?);
+  }
+
   public final class LocationManagerCompat {
     method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void getCurrentLocation(android.location.LocationManager, String, androidx.core.os.CancellationSignal?, java.util.concurrent.Executor, androidx.core.util.Consumer<android.location.Location!>);
     method public static String? getGnssHardwareModelName(android.location.LocationManager);
     method public static int getGnssYearOfHardware(android.location.LocationManager);
+    method public static boolean hasProvider(android.location.LocationManager, String);
     method public static boolean isLocationEnabled(android.location.LocationManager);
     method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public static boolean registerGnssStatusCallback(android.location.LocationManager, androidx.core.location.GnssStatusCompat.Callback, android.os.Handler);
     method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public static boolean registerGnssStatusCallback(android.location.LocationManager, java.util.concurrent.Executor, androidx.core.location.GnssStatusCompat.Callback);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void removeUpdates(android.location.LocationManager, androidx.core.location.LocationListenerCompat);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void requestLocationUpdates(android.location.LocationManager, String, androidx.core.location.LocationRequestCompat, java.util.concurrent.Executor, androidx.core.location.LocationListenerCompat);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void requestLocationUpdates(android.location.LocationManager, String, androidx.core.location.LocationRequestCompat, androidx.core.location.LocationListenerCompat, android.os.Looper);
     method public static void unregisterGnssStatusCallback(android.location.LocationManager, androidx.core.location.GnssStatusCompat.Callback);
   }
 
+  public final class LocationRequestCompat {
+    method @IntRange(from=1) public long getDurationMillis();
+    method @IntRange(from=0) public long getIntervalMillis();
+    method @IntRange(from=0) public long getMaxUpdateDelayMillis();
+    method @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) public int getMaxUpdates();
+    method @FloatRange(from=0, to=java.lang.Float.MAX_VALUE) public float getMinUpdateDistanceMeters();
+    method @IntRange(from=0) public long getMinUpdateIntervalMillis();
+    method public int getQuality();
+    field public static final long PASSIVE_INTERVAL = 9223372036854775807L; // 0x7fffffffffffffffL
+    field public static final int QUALITY_BALANCED_POWER_ACCURACY = 102; // 0x66
+    field public static final int QUALITY_HIGH_ACCURACY = 100; // 0x64
+    field public static final int QUALITY_LOW_POWER = 104; // 0x68
+  }
+
+  public static final class LocationRequestCompat.Builder {
+    ctor public LocationRequestCompat.Builder(long);
+    ctor public LocationRequestCompat.Builder(androidx.core.location.LocationRequestCompat);
+    method public androidx.core.location.LocationRequestCompat build();
+    method public androidx.core.location.LocationRequestCompat.Builder clearMinUpdateIntervalMillis();
+    method public androidx.core.location.LocationRequestCompat.Builder setDurationMillis(@IntRange(from=1) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setIntervalMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setMaxUpdateDelayMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setMaxUpdates(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+    method public androidx.core.location.LocationRequestCompat.Builder setMinUpdateDistanceMeters(@FloatRange(from=0, to=java.lang.Float.MAX_VALUE) float);
+    method public androidx.core.location.LocationRequestCompat.Builder setMinUpdateIntervalMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setQuality(int);
+  }
+
 }
 
 package androidx.core.math {
@@ -1983,11 +2024,16 @@
     method public android.net.Uri? getLinkUri();
     method public int getSource();
     method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+    method @RequiresApi(31) public static android.util.Pair<android.view.ContentInfo!,android.view.ContentInfo!> partition(android.view.ContentInfo, java.util.function.Predicate<android.content.ClipData.Item!>);
+    method @RequiresApi(31) public android.view.ContentInfo toContentInfo();
+    method @RequiresApi(31) public static androidx.core.view.ContentInfoCompat toContentInfoCompat(android.view.ContentInfo);
     field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
     field public static final int SOURCE_APP = 0; // 0x0
+    field public static final int SOURCE_AUTOFILL = 4; // 0x4
     field public static final int SOURCE_CLIPBOARD = 1; // 0x1
     field public static final int SOURCE_DRAG_AND_DROP = 3; // 0x3
     field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+    field public static final int SOURCE_PROCESS_TEXT = 5; // 0x5
   }
 
   public static final class ContentInfoCompat.Builder {
@@ -3413,13 +3459,16 @@
 
   public final class EdgeEffectCompat {
     ctor @Deprecated public EdgeEffectCompat(android.content.Context!);
+    method public static android.widget.EdgeEffect create(android.content.Context, android.util.AttributeSet?);
     method @Deprecated public boolean draw(android.graphics.Canvas!);
     method @Deprecated public void finish();
+    method public static float getDistance(android.widget.EdgeEffect);
     method @Deprecated public boolean isFinished();
     method @Deprecated public boolean onAbsorb(int);
     method @Deprecated public boolean onPull(float);
     method @Deprecated public boolean onPull(float, float);
     method public static void onPull(android.widget.EdgeEffect, float, float);
+    method public static float onPullDistance(android.widget.EdgeEffect, float, float);
     method @Deprecated public boolean onRelease();
     method @Deprecated public void setSize(int, int);
   }
diff --git a/core/core/api/res-current.txt b/core/core/api/res-current.txt
index 36ad356..dd913d3 100644
--- a/core/core/api/res-current.txt
+++ b/core/core/api/res-current.txt
@@ -10,6 +10,7 @@
 attr fontStyle
 attr fontVariationSettings
 attr fontWeight
+attr lStar
 attr queryPatterns
 attr shortcutMatchRequired
 attr ttcIndex
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 86d4e47..0fa5e16 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -44,6 +44,7 @@
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.core.app.ActivityCompat.PermissionCompatDelegate! getPermissionCompatDelegate();
     method public static android.net.Uri? getReferrer(android.app.Activity);
     method @Deprecated public static boolean invalidateOptionsMenu(android.app.Activity!);
+    method public static boolean isLaunchedFromBubble(android.app.Activity);
     method public static void postponeEnterTransition(android.app.Activity);
     method public static void recreate(android.app.Activity);
     method public static androidx.core.view.DragAndDropPermissionsCompat? requestDragAndDropPermissions(android.app.Activity!, android.view.DragEvent!);
@@ -388,6 +389,9 @@
     field public static final int FLAG_ONGOING_EVENT = 2; // 0x2
     field public static final int FLAG_ONLY_ALERT_ONCE = 8; // 0x8
     field public static final int FLAG_SHOW_LIGHTS = 1; // 0x1
+    field public static final int FOREGROUND_SERVICE_DEFAULT = 0; // 0x0
+    field public static final int FOREGROUND_SERVICE_DEFERRED = 2; // 0x2
+    field public static final int FOREGROUND_SERVICE_IMMEDIATE = 1; // 0x1
     field public static final int GROUP_ALERT_ALL = 0; // 0x0
     field public static final int GROUP_ALERT_CHILDREN = 2; // 0x2
     field public static final int GROUP_ALERT_SUMMARY = 1; // 0x1
@@ -548,6 +552,7 @@
     method @ColorInt @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getColor();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.widget.RemoteViews! getContentView();
     method public android.os.Bundle getExtras();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getForegroundServiceBehavior();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.widget.RemoteViews! getHeadsUpContentView();
     method @Deprecated public android.app.Notification getNotification();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getPriority();
@@ -573,6 +578,7 @@
     method public androidx.core.app.NotificationCompat.Builder setDefaults(int);
     method public androidx.core.app.NotificationCompat.Builder setDeleteIntent(android.app.PendingIntent?);
     method public androidx.core.app.NotificationCompat.Builder setExtras(android.os.Bundle?);
+    method public androidx.core.app.NotificationCompat.Builder setForegroundServiceBehavior(@androidx.core.app.NotificationCompat.ServiceNotificationBehavior int);
     method public androidx.core.app.NotificationCompat.Builder setFullScreenIntent(android.app.PendingIntent?, boolean);
     method public androidx.core.app.NotificationCompat.Builder setGroup(String?);
     method public androidx.core.app.NotificationCompat.Builder setGroupAlertBehavior(@androidx.core.app.NotificationCompat.GroupAlertBehavior int);
@@ -700,6 +706,9 @@
   @IntDef({androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC, androidx.core.app.NotificationCompat.VISIBILITY_PRIVATE, androidx.core.app.NotificationCompat.VISIBILITY_SECRET}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface NotificationCompat.NotificationVisibility {
   }
 
+  @IntDef({androidx.core.app.NotificationCompat.FOREGROUND_SERVICE_DEFAULT, androidx.core.app.NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE, androidx.core.app.NotificationCompat.FOREGROUND_SERVICE_DEFERRED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface NotificationCompat.ServiceNotificationBehavior {
+  }
+
   @IntDef({android.media.AudioManager.STREAM_VOICE_CALL, android.media.AudioManager.STREAM_SYSTEM, android.media.AudioManager.STREAM_RING, android.media.AudioManager.STREAM_MUSIC, android.media.AudioManager.STREAM_ALARM, android.media.AudioManager.STREAM_NOTIFICATION, android.media.AudioManager.STREAM_DTMF, android.media.AudioManager.STREAM_ACCESSIBILITY}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface NotificationCompat.StreamType {
   }
 
@@ -1794,16 +1803,52 @@
     method public static void setMock(android.location.Location, boolean);
   }
 
+  public interface LocationListenerCompat extends android.location.LocationListener {
+    method public default void onStatusChanged(String, int, android.os.Bundle?);
+  }
+
   public final class LocationManagerCompat {
     method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void getCurrentLocation(android.location.LocationManager, String, androidx.core.os.CancellationSignal?, java.util.concurrent.Executor, androidx.core.util.Consumer<android.location.Location!>);
     method public static String? getGnssHardwareModelName(android.location.LocationManager);
     method public static int getGnssYearOfHardware(android.location.LocationManager);
+    method public static boolean hasProvider(android.location.LocationManager, String);
     method public static boolean isLocationEnabled(android.location.LocationManager);
     method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public static boolean registerGnssStatusCallback(android.location.LocationManager, androidx.core.location.GnssStatusCompat.Callback, android.os.Handler);
     method @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) public static boolean registerGnssStatusCallback(android.location.LocationManager, java.util.concurrent.Executor, androidx.core.location.GnssStatusCompat.Callback);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void removeUpdates(android.location.LocationManager, androidx.core.location.LocationListenerCompat);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void requestLocationUpdates(android.location.LocationManager, String, androidx.core.location.LocationRequestCompat, java.util.concurrent.Executor, androidx.core.location.LocationListenerCompat);
+    method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public static void requestLocationUpdates(android.location.LocationManager, String, androidx.core.location.LocationRequestCompat, androidx.core.location.LocationListenerCompat, android.os.Looper);
     method public static void unregisterGnssStatusCallback(android.location.LocationManager, androidx.core.location.GnssStatusCompat.Callback);
   }
 
+  public final class LocationRequestCompat {
+    method @IntRange(from=1) public long getDurationMillis();
+    method @IntRange(from=0) public long getIntervalMillis();
+    method @IntRange(from=0) public long getMaxUpdateDelayMillis();
+    method @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) public int getMaxUpdates();
+    method @FloatRange(from=0, to=java.lang.Float.MAX_VALUE) public float getMinUpdateDistanceMeters();
+    method @IntRange(from=0) public long getMinUpdateIntervalMillis();
+    method public int getQuality();
+    field public static final long PASSIVE_INTERVAL = 9223372036854775807L; // 0x7fffffffffffffffL
+    field public static final int QUALITY_BALANCED_POWER_ACCURACY = 102; // 0x66
+    field public static final int QUALITY_HIGH_ACCURACY = 100; // 0x64
+    field public static final int QUALITY_LOW_POWER = 104; // 0x68
+  }
+
+  public static final class LocationRequestCompat.Builder {
+    ctor public LocationRequestCompat.Builder(long);
+    ctor public LocationRequestCompat.Builder(androidx.core.location.LocationRequestCompat);
+    method public androidx.core.location.LocationRequestCompat build();
+    method public androidx.core.location.LocationRequestCompat.Builder clearMinUpdateIntervalMillis();
+    method public androidx.core.location.LocationRequestCompat.Builder setDurationMillis(@IntRange(from=1) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setIntervalMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setMaxUpdateDelayMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setMaxUpdates(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+    method public androidx.core.location.LocationRequestCompat.Builder setMinUpdateDistanceMeters(@FloatRange(from=0, to=java.lang.Float.MAX_VALUE) float);
+    method public androidx.core.location.LocationRequestCompat.Builder setMinUpdateIntervalMillis(@IntRange(from=0) long);
+    method public androidx.core.location.LocationRequestCompat.Builder setQuality(int);
+  }
+
 }
 
 package androidx.core.math {
@@ -2298,7 +2343,11 @@
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class Preconditions {
     method public static void checkArgument(boolean);
     method public static void checkArgument(boolean, Object);
+    method public static void checkArgument(boolean, String, java.lang.Object!...);
     method public static int checkArgumentInRange(int, int, int, String);
+    method public static long checkArgumentInRange(long, long, long, String);
+    method public static float checkArgumentInRange(float, float, float, String);
+    method public static double checkArgumentInRange(double, double, double, String);
     method @IntRange(from=0) public static int checkArgumentNonnegative(int, String?);
     method @IntRange(from=0) public static int checkArgumentNonnegative(int);
     method public static int checkFlagsArgument(int, int);
@@ -2377,11 +2426,16 @@
     method public android.net.Uri? getLinkUri();
     method @androidx.core.view.ContentInfoCompat.Source public int getSource();
     method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+    method @RequiresApi(31) public static android.util.Pair<android.view.ContentInfo!,android.view.ContentInfo!> partition(android.view.ContentInfo, java.util.function.Predicate<android.content.ClipData.Item!>);
+    method @RequiresApi(31) public android.view.ContentInfo toContentInfo();
+    method @RequiresApi(31) public static androidx.core.view.ContentInfoCompat toContentInfoCompat(android.view.ContentInfo);
     field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
     field public static final int SOURCE_APP = 0; // 0x0
+    field public static final int SOURCE_AUTOFILL = 4; // 0x4
     field public static final int SOURCE_CLIPBOARD = 1; // 0x1
     field public static final int SOURCE_DRAG_AND_DROP = 3; // 0x3
     field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+    field public static final int SOURCE_PROCESS_TEXT = 5; // 0x5
   }
 
   public static final class ContentInfoCompat.Builder {
@@ -2398,7 +2452,7 @@
   @IntDef(flag=true, value={androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ContentInfoCompat.Flags {
   }
 
-  @IntDef({androidx.core.view.ContentInfoCompat.SOURCE_APP, androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD, androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD, androidx.core.view.ContentInfoCompat.SOURCE_DRAG_AND_DROP}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ContentInfoCompat.Source {
+  @IntDef({androidx.core.view.ContentInfoCompat.SOURCE_APP, androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD, androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD, androidx.core.view.ContentInfoCompat.SOURCE_DRAG_AND_DROP, androidx.core.view.ContentInfoCompat.SOURCE_AUTOFILL, androidx.core.view.ContentInfoCompat.SOURCE_PROCESS_TEXT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ContentInfoCompat.Source {
   }
 
   public final class DisplayCompat {
@@ -3873,13 +3927,16 @@
 
   public final class EdgeEffectCompat {
     ctor @Deprecated public EdgeEffectCompat(android.content.Context!);
+    method public static android.widget.EdgeEffect create(android.content.Context, android.util.AttributeSet?);
     method @Deprecated public boolean draw(android.graphics.Canvas!);
     method @Deprecated public void finish();
+    method public static float getDistance(android.widget.EdgeEffect);
     method @Deprecated public boolean isFinished();
     method @Deprecated public boolean onAbsorb(int);
     method @Deprecated public boolean onPull(float);
     method @Deprecated public boolean onPull(float, float);
     method public static void onPull(android.widget.EdgeEffect, float, float);
+    method public static float onPullDistance(android.widget.EdgeEffect, float, float);
     method @Deprecated public boolean onRelease();
     method @Deprecated public void setSize(int, int);
   }
diff --git a/core/core/build.gradle b/core/core/build.gradle
index fe66457..d915ff3 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -74,6 +74,10 @@
     defaultConfig {
         multiDexEnabled = true
     }
+    // TODO(aurimas): reenable once AGP 7.0-alpha15 lands
+    lintOptions {
+        disable("ClassVerificationFailure", "NewApi")
+    }
 }
 
 androidx {
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index a2d6eef..570acb0 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -3748,7 +3748,7 @@
         errorLine2="                         ~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="60"
+            line="62"
             column="26"/>
     </issue>
 
@@ -3759,7 +3759,7 @@
         errorLine2="                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="60"
+            line="62"
             column="40"/>
     </issue>
 
@@ -3770,7 +3770,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="118"
+            line="120"
             column="26"/>
     </issue>
 
@@ -3781,7 +3781,7 @@
         errorLine2="                         ~~~~~~~~">
         <location
             file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="163"
+            line="165"
             column="26"/>
     </issue>
 
@@ -3792,7 +3792,7 @@
         errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="223"
+            line="225"
             column="26"/>
     </issue>
 
@@ -4661,7 +4661,7 @@
         errorLine2="                       ~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/EdgeEffectCompat.java"
-            line="153"
+            line="238"
             column="24"/>
     </issue>
 
@@ -5999,45 +5999,45 @@
     <issue
         id="ClassVerificationFailure"
         message="This call references a method added in API level 21; however, the containing class androidx.core.widget.NestedScrollView is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                if (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {"
-        errorLine2="                                                                            ~~~~~~~~~~~~~~~~">
+        errorLine1="            if (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {"
+        errorLine2="                                                                        ~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="2008"
-            column="77"/>
+            line="2075"
+            column="73"/>
     </issue>
 
     <issue
         id="ClassVerificationFailure"
         message="This call references a method added in API level 21; however, the containing class androidx.core.widget.NestedScrollView is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &amp;&amp; getClipToPadding()) {"
-        errorLine2="                                                                             ~~~~~~~~~~~~~~~~">
+        errorLine1="            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &amp;&amp; getClipToPadding()) {"
+        errorLine2="                                                                         ~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="2012"
-            column="78"/>
+            line="2079"
+            column="74"/>
     </issue>
 
     <issue
         id="ClassVerificationFailure"
         message="This call references a method added in API level 21; however, the containing class androidx.core.widget.NestedScrollView is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                if (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {"
-        errorLine2="                                                                            ~~~~~~~~~~~~~~~~">
+        errorLine1="            if (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {"
+        errorLine2="                                                                        ~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="2029"
-            column="77"/>
+            line="2096"
+            column="73"/>
     </issue>
 
     <issue
         id="ClassVerificationFailure"
         message="This call references a method added in API level 21; however, the containing class androidx.core.widget.NestedScrollView is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &amp;&amp; getClipToPadding()) {"
-        errorLine2="                                                                             ~~~~~~~~~~~~~~~~">
+        errorLine1="            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &amp;&amp; getClipToPadding()) {"
+        errorLine2="                                                                         ~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="2033"
-            column="78"/>
+            line="2100"
+            column="74"/>
     </issue>
 
     <issue
@@ -10920,7 +10920,7 @@
         errorLine2="                                                               ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java"
-            line="801"
+            line="800"
             column="64"/>
     </issue>
 
@@ -14810,7 +14810,7 @@
         errorLine2="                            ~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/EdgeEffectCompat.java"
-            line="47"
+            line="67"
             column="29"/>
     </issue>
 
@@ -14821,7 +14821,7 @@
         errorLine2="                        ~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/EdgeEffectCompat.java"
-            line="207"
+            line="337"
             column="25"/>
     </issue>
 
@@ -16449,7 +16449,7 @@
         errorLine2="                              ~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="247"
+            line="272"
             column="31"/>
     </issue>
 
@@ -16460,7 +16460,7 @@
         errorLine2="                                                           ~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="253"
+            line="278"
             column="60"/>
     </issue>
 
@@ -16471,7 +16471,7 @@
         errorLine2="                                                                           ~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="253"
+            line="278"
             column="76"/>
     </issue>
 
@@ -16482,7 +16482,7 @@
         errorLine2="                              ~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="287"
+            line="312"
             column="31"/>
     </issue>
 
@@ -16493,7 +16493,7 @@
         errorLine2="                                                           ~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="293"
+            line="318"
             column="60"/>
     </issue>
 
@@ -16504,7 +16504,7 @@
         errorLine2="                                                                           ~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="293"
+            line="318"
             column="76"/>
     </issue>
 
@@ -16515,7 +16515,7 @@
         errorLine2="                        ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="472"
+            line="497"
             column="25"/>
     </issue>
 
@@ -16526,7 +16526,7 @@
         errorLine2="                        ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="481"
+            line="506"
             column="25"/>
     </issue>
 
@@ -16537,7 +16537,7 @@
         errorLine2="                        ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="490"
+            line="515"
             column="25"/>
     </issue>
 
@@ -16548,7 +16548,7 @@
         errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="490"
+            line="515"
             column="37"/>
     </issue>
 
@@ -16559,7 +16559,7 @@
         errorLine2="                        ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="499"
+            line="524"
             column="25"/>
     </issue>
 
@@ -16570,7 +16570,7 @@
         errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="499"
+            line="524"
             column="48"/>
     </issue>
 
@@ -16581,7 +16581,7 @@
         errorLine2="                                    ~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="620"
+            line="645"
             column="37"/>
     </issue>
 
@@ -16592,7 +16592,7 @@
         errorLine2="                                         ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="717"
+            line="742"
             column="42"/>
     </issue>
 
@@ -16603,7 +16603,7 @@
         errorLine2="                                ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="828"
+            line="854"
             column="33"/>
     </issue>
 
@@ -16614,7 +16614,7 @@
         errorLine2="                                        ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1011"
+            line="1061"
             column="41"/>
     </issue>
 
@@ -16625,7 +16625,7 @@
         errorLine2="                                ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1574"
+            line="1624"
             column="33"/>
     </issue>
 
@@ -16636,7 +16636,7 @@
         errorLine2="                                           ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1590"
+            line="1640"
             column="44"/>
     </issue>
 
@@ -16647,7 +16647,7 @@
         errorLine2="                                                           ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1725"
+            line="1808"
             column="60"/>
     </issue>
 
@@ -16658,7 +16658,7 @@
         errorLine2="                                  ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1791"
+            line="1874"
             column="35"/>
     </issue>
 
@@ -16669,7 +16669,7 @@
         errorLine2="                                              ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1791"
+            line="1874"
             column="47"/>
     </issue>
 
@@ -16680,7 +16680,7 @@
         errorLine2="            ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1811"
+            line="1894"
             column="13"/>
     </issue>
 
@@ -16691,7 +16691,7 @@
         errorLine2="                                                 ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1838"
+            line="1921"
             column="50"/>
     </issue>
 
@@ -16702,7 +16702,7 @@
         errorLine2="                                                             ~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1838"
+            line="1921"
             column="62"/>
     </issue>
 
@@ -16713,7 +16713,7 @@
         errorLine2="                     ~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="1998"
+            line="2066"
             column="22"/>
     </issue>
 
@@ -16724,7 +16724,7 @@
         errorLine2="                                          ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="2079"
+            line="2145"
             column="43"/>
     </issue>
 
@@ -16735,7 +16735,7 @@
         errorLine2="              ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/core/widget/NestedScrollView.java"
-            line="2092"
+            line="2158"
             column="15"/>
     </issue>
 
diff --git a/core/core/proguard-rules.pro b/core/core/proguard-rules.pro
index 47a95b5..bcc9a89 100644
--- a/core/core/proguard-rules.pro
+++ b/core/core/proguard-rules.pro
@@ -11,3 +11,6 @@
 -keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.os.UserHandleCompat$Api*Impl {
   <methods>;
 }
+-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.widget.EdgeEffectCompat$Api*Impl {
+  <methods>;
+}
diff --git a/core/core/src/androidTest/AndroidManifest.xml b/core/core/src/androidTest/AndroidManifest.xml
index acea6c4..6a884e9 100644
--- a/core/core/src/androidTest/AndroidManifest.xml
+++ b/core/core/src/androidTest/AndroidManifest.xml
@@ -88,6 +88,8 @@
 
         <activity android:name="androidx.core.app.FrameMetricsActivity"/>
         <activity android:name="androidx.core.app.FrameMetricsSubActivity"/>
+        <activity
+            android:name="androidx.core.widget.EdgeEffectCompatTest$EdgeEffectCompatTestActivity"/>
 
         <activity
             android:name="androidx.core.view.WindowCompatActivity"
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index 53eebef..8ce5db7 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -559,6 +559,18 @@
 
     @FlakyTest(bugId = 190533219)
     @Test
+    public void testNotificationBuilder_foregroundServiceBehavior() {
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "channelId");
+        assertEquals(NotificationCompat.FOREGROUND_SERVICE_DEFAULT,
+                builder.getForegroundServiceBehavior());
+        builder.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE);
+        assertEquals(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE,
+                builder.getForegroundServiceBehavior());
+
+        // TODO: validate the built Notifications once there's testing API there
+    }
+
+    @Test
     public void testNotificationBuilder_createContentView() {
         NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, "channelId");
         assertNull(builder.getContentView());
diff --git a/core/core/src/androidTest/java/androidx/core/content/res/ColorStateListInflaterCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/res/ColorStateListInflaterCompatTest.java
index 72572a4..0a74104 100644
--- a/core/core/src/androidTest/java/androidx/core/content/res/ColorStateListInflaterCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/res/ColorStateListInflaterCompatTest.java
@@ -29,9 +29,11 @@
 
 import androidx.annotation.AttrRes;
 import androidx.annotation.ColorInt;
+import androidx.core.graphics.ColorUtils;
 import androidx.core.test.R;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
@@ -93,6 +95,46 @@
         assertEquals(expectedTextColorSecondary, result.getDefaultColor());
     }
 
+    @Test
+    public void testCreateFromXmlWithAppLStar() {
+        final double lStarInXml = 50.0;
+        final int alphaInXml = 128;
+
+        final Resources res = mContext.getResources();
+        final ColorStateList c = ColorStateListInflaterCompat.inflate(
+                res, R.color.color_state_list_lstar, mContext.getTheme());
+        final int defaultColor = c.getDefaultColor();
+
+        final double[] labColor = new double[3];
+        ColorUtils.colorToLAB(defaultColor, labColor);
+
+        // There's precision loss when converting to @ColorInt. We need a small delta.
+        assertEquals(lStarInXml, labColor[0], 1.0 /* delta */);
+        assertEquals(alphaInXml, Color.alpha(defaultColor));
+    }
+
+    /**
+     * Tests using android:lStar which is added in Android S
+     */
+    @Test
+    @SdkSuppress(minSdkVersion = 31, codeName = "S")
+    public void testCreateFromXmlWithAndroidLStar() {
+        final double lStarInXml = 50.0;
+        final int alphaInXml = 128;
+
+        final Resources res = mContext.getResources();
+        final ColorStateList c = ColorStateListInflaterCompat.inflate(
+                res, R.color.color_state_list_android_lstar, mContext.getTheme());
+        final int defaultColor = c.getDefaultColor();
+
+        final double[] labColor = new double[3];
+        ColorUtils.colorToLAB(defaultColor, labColor);
+
+        // There's precision loss when converting to @ColorInt. We need a small delta.
+        assertEquals(lStarInXml, labColor[0], 1.0 /* delta */);
+        assertEquals(alphaInXml, Color.alpha(defaultColor));
+    }
+
     @ColorInt
     private int getColorFromTheme(@AttrRes int attrResId) {
         TypedArray a = TypedArrayUtils.obtainAttributes(mResources, mContext.getTheme(), null,
diff --git a/core/core/src/androidTest/java/androidx/core/location/LocationManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/location/LocationManagerCompatTest.java
index a8a5399..c59abd0 100644
--- a/core/core/src/androidTest/java/androidx/core/location/LocationManagerCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/location/LocationManagerCompatTest.java
@@ -19,11 +19,12 @@
 import static android.provider.Settings.Secure.LOCATION_MODE;
 import static android.provider.Settings.Secure.LOCATION_MODE_OFF;
 
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
-import android.location.Location;
 import android.location.LocationManager;
 import android.os.Build;
 import android.os.Handler;
@@ -33,7 +34,6 @@
 
 import androidx.core.os.CancellationSignal;
 import androidx.core.os.ExecutorCompat;
-import androidx.core.util.Consumer;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -76,20 +76,68 @@
     }
 
     @Test
+    public void testHasProvider() {
+        for (String provider : mLocationManager.getAllProviders()) {
+            boolean hasProvider;
+            if (Build.VERSION.SDK_INT >= 31) {
+                hasProvider = mLocationManager.hasProvider(provider);
+            } else {
+                hasProvider = mLocationManager.getProvider(provider) != null;
+            }
+
+            assertEquals(hasProvider, LocationManagerCompat.hasProvider(mLocationManager,
+                    provider));
+        }
+    }
+
+    @Test
     public void testGetCurrentLocation() {
         // can't do much to test this except check it doesn't crash
         CancellationSignal cs = new CancellationSignal();
         LocationManagerCompat.getCurrentLocation(mLocationManager,
                 LocationManager.PASSIVE_PROVIDER, cs,
                 ExecutorCompat.create(new Handler(Looper.getMainLooper())),
-                new Consumer<Location>() {
-                    @Override
-                    public void accept(Location location) {}
-                });
+                location -> {});
         cs.cancel();
     }
 
     @Test
+    public void testRequestLocationUpdates_Executor() {
+        // can't do much to test this except check it doesn't crash
+        LocationRequestCompat request = new LocationRequestCompat.Builder(0).build();
+        LocationListenerCompat listener1 = location -> {};
+        LocationListenerCompat listener2 = location -> {};
+        for (String provider : mLocationManager.getAllProviders()) {
+            LocationManagerCompat.requestLocationUpdates(mLocationManager, provider, request,
+                    directExecutor(), listener1);
+            LocationManagerCompat.requestLocationUpdates(mLocationManager, provider, request,
+                    directExecutor(), listener2);
+            LocationManagerCompat.requestLocationUpdates(mLocationManager, provider, request,
+                    directExecutor(), listener1);
+        }
+        LocationManagerCompat.removeUpdates(mLocationManager, listener1);
+        LocationManagerCompat.removeUpdates(mLocationManager, listener2);
+    }
+
+    @Test
+    public void testRequestLocationUpdates_Looper() {
+        // can't do much to test this except check it doesn't crash
+        LocationRequestCompat request = new LocationRequestCompat.Builder(0).build();
+        LocationListenerCompat listener1 = location -> {};
+        LocationListenerCompat listener2 = location -> {};
+        for (String provider : mLocationManager.getAllProviders()) {
+            LocationManagerCompat.requestLocationUpdates(mLocationManager, provider, request,
+                    listener1, Looper.getMainLooper());
+            LocationManagerCompat.requestLocationUpdates(mLocationManager, provider, request,
+                    listener2, Looper.getMainLooper());
+            LocationManagerCompat.requestLocationUpdates(mLocationManager, provider, request,
+                    listener1, Looper.getMainLooper());
+        }
+        LocationManagerCompat.removeUpdates(mLocationManager, listener1);
+        LocationManagerCompat.removeUpdates(mLocationManager, listener2);
+    }
+
+    @Test
     public void testGetGnssHardwareModelName() {
         // can't do much to test this except check it doesn't crash
         LocationManagerCompat.getGnssHardwareModelName(mLocationManager);
diff --git a/core/core/src/androidTest/java/androidx/core/location/LocationRequestCompatTest.java b/core/core/src/androidTest/java/androidx/core/location/LocationRequestCompatTest.java
new file mode 100644
index 0000000..4500b2c
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/location/LocationRequestCompatTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2021 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 androidx.core.location;
+
+import static androidx.core.location.LocationRequestCompat.PASSIVE_INTERVAL;
+import static androidx.core.location.LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY;
+import static androidx.core.location.LocationRequestCompat.QUALITY_HIGH_ACCURACY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import android.location.LocationRequest;
+import android.os.SystemClock;
+
+import androidx.core.util.Preconditions;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.Range;
+
+import org.junit.Test;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@SmallTest
+public class LocationRequestCompatTest {
+
+    private static Method sGetProviderMethod;
+    private static Method sGetIntervalMethod;
+    private static Method sGetFastestIntervalMethod;
+    private static Method sGetExpireAtMethod;
+    private static Method sGetNumUpdatesMethod;
+    private static Method sGetSmallestDisplacementMethod;
+
+    @Test
+    public void testBuilder() {
+        LocationRequestCompat.Builder builder = new LocationRequestCompat.Builder(0);
+
+        assertEquals(QUALITY_BALANCED_POWER_ACCURACY, builder.build().getQuality());
+        assertEquals(0, builder.build().getIntervalMillis());
+        assertEquals(0, builder.build().getMinUpdateIntervalMillis());
+        assertEquals(Long.MAX_VALUE, builder.build().getDurationMillis());
+        assertEquals(Integer.MAX_VALUE, builder.build().getMaxUpdates());
+        assertEquals(0, builder.build().getMinUpdateDistanceMeters(), 0);
+        assertEquals(0, builder.build().getMaxUpdateDelayMillis());
+
+        builder.setQuality(QUALITY_HIGH_ACCURACY);
+        assertEquals(QUALITY_HIGH_ACCURACY, builder.build().getQuality());
+
+        builder.setIntervalMillis(1000);
+        assertEquals(1000, builder.build().getIntervalMillis());
+
+        builder.setMinUpdateIntervalMillis(1500);
+        assertEquals(1000, builder.build().getMinUpdateIntervalMillis());
+
+        builder.setMinUpdateIntervalMillis(500);
+        assertEquals(500, builder.build().getMinUpdateIntervalMillis());
+
+        builder.clearMinUpdateIntervalMillis();
+        assertEquals(1000, builder.build().getMinUpdateIntervalMillis());
+
+        builder.setDurationMillis(1);
+        assertEquals(1, builder.build().getDurationMillis());
+
+        builder.setMaxUpdates(1);
+        assertEquals(1, builder.build().getMaxUpdates());
+
+        builder.setMinUpdateDistanceMeters(10);
+        assertEquals(10, builder.build().getMinUpdateDistanceMeters(), 0);
+
+        builder.setMaxUpdateDelayMillis(10000);
+        assertEquals(10000, builder.build().getMaxUpdateDelayMillis());
+
+        builder.setIntervalMillis(PASSIVE_INTERVAL);
+        builder.setMinUpdateIntervalMillis(0);
+        assertEquals(PASSIVE_INTERVAL, builder.build().getIntervalMillis());
+    }
+
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 30)
+    @Test
+    public void testConversion_19Plus() throws Exception {
+        LocationRequestCompat.Builder builder = new LocationRequestCompat.Builder(0);
+
+        assertEquals("test", getProvider(builder.build().toLocationRequest("test")));
+        assertEquals(QUALITY_BALANCED_POWER_ACCURACY,
+                builder.build().toLocationRequest("test").getQuality());
+        assertEquals(0, getInterval(builder.build().toLocationRequest("test")));
+        assertEquals(0, getFastestInterval(builder.build().toLocationRequest("test")));
+        assertEquals(Long.MAX_VALUE, getExpireAt(builder.build().toLocationRequest()));
+        assertEquals(Integer.MAX_VALUE, getNumUpdates(builder.build().toLocationRequest("test")));
+        assertEquals(0, getSmallestDisplacement(builder.build().toLocationRequest("test")), 0);
+
+        builder.setQuality(QUALITY_HIGH_ACCURACY);
+        assertEquals(QUALITY_HIGH_ACCURACY, builder.build().toLocationRequest("test").getQuality());
+
+        builder.setIntervalMillis(1000);
+        assertEquals(1000, getInterval(builder.build().toLocationRequest("test")));
+
+        builder.setMinUpdateIntervalMillis(1500);
+        assertEquals(1000, getFastestInterval(builder.build().toLocationRequest("test")));
+
+        builder.setMinUpdateIntervalMillis(500);
+        assertEquals(500, getFastestInterval(builder.build().toLocationRequest("test")));
+
+        builder.clearMinUpdateIntervalMillis();
+        assertEquals(1000, getFastestInterval(builder.build().toLocationRequest("test")));
+
+        builder.setDurationMillis(1);
+        long time = SystemClock.elapsedRealtime();
+        assertThat(getExpireAt(builder.build().toLocationRequest())).isIn(
+                Range.closed(time - 1000, time));
+
+        builder.setMaxUpdates(1);
+        assertEquals(1, getNumUpdates(builder.build().toLocationRequest("test")));
+
+        builder.setMinUpdateDistanceMeters(10);
+        assertEquals(10, getSmallestDisplacement(builder.build().toLocationRequest("test")), 0);
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testConversion_31Plus() {
+        LocationRequestCompat.Builder builder = new LocationRequestCompat.Builder(0);
+
+        assertEquals(QUALITY_BALANCED_POWER_ACCURACY,
+                builder.build().toLocationRequest().getQuality());
+        assertEquals(0, builder.build().toLocationRequest().getIntervalMillis());
+        assertEquals(0, builder.build().toLocationRequest().getMinUpdateIntervalMillis());
+        assertEquals(Long.MAX_VALUE, builder.build().toLocationRequest().getDurationMillis());
+        assertEquals(Integer.MAX_VALUE, builder.build().toLocationRequest().getMaxUpdates());
+        assertEquals(0, builder.build().toLocationRequest().getMinUpdateDistanceMeters(), 0);
+        assertEquals(0, builder.build().toLocationRequest().getMaxUpdateDelayMillis());
+
+        builder.setQuality(QUALITY_HIGH_ACCURACY);
+        assertEquals(QUALITY_HIGH_ACCURACY, builder.build().toLocationRequest().getQuality());
+
+        builder.setIntervalMillis(1000);
+        assertEquals(1000, builder.build().toLocationRequest().getIntervalMillis());
+
+        builder.setMinUpdateIntervalMillis(1500);
+        assertEquals(1000, builder.build().toLocationRequest().getMinUpdateIntervalMillis());
+
+        builder.setMinUpdateIntervalMillis(500);
+        assertEquals(500, builder.build().toLocationRequest().getMinUpdateIntervalMillis());
+
+        builder.clearMinUpdateIntervalMillis();
+        assertEquals(1000, builder.build().toLocationRequest().getMinUpdateIntervalMillis());
+
+        builder.setDurationMillis(1);
+        assertEquals(1, builder.build().toLocationRequest().getDurationMillis());
+
+        builder.setMaxUpdates(1);
+        assertEquals(1, builder.build().toLocationRequest().getMaxUpdates());
+
+        builder.setMinUpdateDistanceMeters(10);
+        assertEquals(10, builder.build().toLocationRequest().getMinUpdateDistanceMeters(), 0);
+
+        builder.setMaxUpdateDelayMillis(10000);
+        assertEquals(10000, builder.build().toLocationRequest().getMaxUpdateDelayMillis());
+
+        builder.setMinUpdateIntervalMillis(1000);
+        builder.setIntervalMillis(PASSIVE_INTERVAL);
+        assertEquals(PASSIVE_INTERVAL,
+                builder.build().toLocationRequest().getIntervalMillis());
+    }
+
+    private static String getProvider(LocationRequest request)
+            throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
+        if (sGetProviderMethod == null) {
+            sGetProviderMethod = LocationRequest.class.getDeclaredMethod("getProvider");
+            sGetProviderMethod.setAccessible(true);
+        }
+
+        return (String) sGetProviderMethod.invoke(request);
+    }
+
+    private static long getInterval(LocationRequest request)
+            throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
+        if (sGetIntervalMethod == null) {
+            sGetIntervalMethod = LocationRequest.class.getDeclaredMethod("getInterval");
+            sGetIntervalMethod.setAccessible(true);
+        }
+
+        return (Long) Preconditions.checkNotNull(sGetIntervalMethod.invoke(request));
+    }
+
+    private static long getFastestInterval(LocationRequest request)
+            throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
+        if (sGetFastestIntervalMethod == null) {
+            sGetFastestIntervalMethod = LocationRequest.class.getDeclaredMethod(
+                    "getFastestInterval");
+            sGetFastestIntervalMethod.setAccessible(true);
+        }
+
+        return (Long) Preconditions.checkNotNull(sGetFastestIntervalMethod.invoke(request));
+    }
+
+    private static long getExpireAt(LocationRequest request)
+            throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
+        if (sGetExpireAtMethod == null) {
+            sGetExpireAtMethod = LocationRequest.class.getDeclaredMethod("getExpireAt");
+            sGetExpireAtMethod.setAccessible(true);
+        }
+
+        return (Long) Preconditions.checkNotNull(sGetExpireAtMethod.invoke(request));
+    }
+
+    private static int getNumUpdates(LocationRequest request)
+            throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
+        if (sGetNumUpdatesMethod == null) {
+            sGetNumUpdatesMethod = LocationRequest.class.getDeclaredMethod("getNumUpdates");
+            sGetNumUpdatesMethod.setAccessible(true);
+        }
+
+        return (Integer) Preconditions.checkNotNull(sGetNumUpdatesMethod.invoke(request));
+    }
+
+    private static float getSmallestDisplacement(LocationRequest request)
+            throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
+        if (sGetSmallestDisplacementMethod == null) {
+            sGetSmallestDisplacementMethod = LocationRequest.class.getDeclaredMethod(
+                    "getSmallestDisplacement");
+            sGetSmallestDisplacementMethod.setAccessible(true);
+        }
+
+        return (Float) Preconditions.checkNotNull(sGetSmallestDisplacementMethod.invoke(request));
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
index 74dd573..9c5f09a 100644
--- a/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
@@ -16,6 +16,8 @@
 
 package androidx.core.view;
 
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_APP;
 import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -24,11 +26,14 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Pair;
+import android.view.ContentInfo;
 
 import androidx.core.util.Predicate;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -43,7 +48,7 @@
         clip.addItem(new ClipData.Item("Hi"));
         clip.addItem(new ClipData.Item(sampleUri));
         ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
-                .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setFlags(FLAG_CONVERT_TO_PLAIN_TEXT)
                 .setLinkUri(Uri.parse("http://example.com"))
                 .setExtras(new Bundle())
                 .build();
@@ -95,7 +100,7 @@
     public void testPartition_singleItem() throws Exception {
         ClipData clip = ClipData.newPlainText("", "Hello");
         ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
-                .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setFlags(FLAG_CONVERT_TO_PLAIN_TEXT)
                 .setLinkUri(Uri.parse("http://example.com"))
                 .setExtras(new Bundle())
                 .build();
@@ -119,4 +124,247 @@
         assertThat(split.first).isSameInstanceAs(payload);
         assertThat(split.second).isNull();
     }
+
+    @Test
+    public void testBuilder_validation() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+
+        // Test validation of source.
+        ContentInfoCompat.Builder builder = new ContentInfoCompat.Builder(clip, 6);
+        try {
+            ContentInfoCompat payload = builder.build();
+            Assert.fail("Expected exception but got: " + payload);
+        } catch (IllegalArgumentException expected) {
+        }
+
+        // Test validation of flags.
+        builder = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD).setFlags(1 << 1);
+        try {
+            ContentInfoCompat payload = builder.build();
+            Assert.fail("Expected exception but got: " + payload);
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuilder_copy() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        ContentInfoCompat original = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
+                .setFlags(FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Verify that that calling the builder with a ContentInfoCompat instance creates a
+        // shallow copy.
+        ContentInfoCompat copy = new ContentInfoCompat.Builder(original).build();
+        assertThat(copy).isNotSameInstanceAs(original);
+        assertThat(copy.getClip()).isSameInstanceAs(original.getClip());
+        assertThat(copy.getSource()).isEqualTo(original.getSource());
+        assertThat(copy.getFlags()).isEqualTo(original.getFlags());
+        assertThat(copy.getLinkUri()).isSameInstanceAs(original.getLinkUri());
+        assertThat(copy.getExtras()).isSameInstanceAs(original.getExtras());
+    }
+
+    @Test
+    public void testBuilder_copyAndUpdate() throws Exception {
+        ClipData clip1 = ClipData.newPlainText("", "Hello");
+        ContentInfoCompat original = new ContentInfoCompat.Builder(clip1, SOURCE_CLIPBOARD)
+                .setFlags(FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Verify that calling setters after initializing the builder with a ContentInfoCompat
+        // instance updates the fields.
+        ClipData clip2 = ClipData.newPlainText("", "Bye");
+        ContentInfoCompat copy = new ContentInfoCompat.Builder(original)
+                .setClip(clip2)
+                .setSource(SOURCE_APP)
+                .setFlags(0)
+                .setLinkUri(null)
+                .setExtras(null)
+                .build();
+        assertThat(copy.getClip().getItemAt(0).getText()).isEqualTo("Bye");
+        assertThat(copy.getSource()).isEqualTo(SOURCE_APP);
+        assertThat(copy.getFlags()).isEqualTo(0);
+        assertThat(copy.getLinkUri()).isEqualTo(null);
+        assertThat(copy.getExtras()).isEqualTo(null);
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testBuilder_copyAndUpdate_platformContentInfo() throws Exception {
+        ClipData clip1 = ClipData.newPlainText("", "Hello");
+        ContentInfoCompat original = new ContentInfoCompat.Builder(clip1, SOURCE_CLIPBOARD)
+                .setFlags(FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Verify that calling setters after initializing the builder with a ContentInfoCompat
+        // instance updates the wrapped platform object also.
+        ClipData clip2 = ClipData.newPlainText("", "Bye");
+        ContentInfoCompat copy = new ContentInfoCompat.Builder(original)
+                .setClip(clip2)
+                .setSource(SOURCE_APP)
+                .setFlags(0)
+                .setLinkUri(null)
+                .setExtras(null)
+                .build();
+        ContentInfo platContentInfo = copy.toContentInfo();
+        assertThat(platContentInfo).isNotSameInstanceAs(original.toContentInfo());
+        assertThat(platContentInfo.getClip().getItemAt(0).getText()).isEqualTo("Bye");
+        assertThat(platContentInfo.getSource()).isEqualTo(SOURCE_APP);
+        assertThat(platContentInfo.getFlags()).isEqualTo(0);
+        assertThat(platContentInfo.getLinkUri()).isEqualTo(null);
+        assertThat(platContentInfo.getExtras()).isEqualTo(null);
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testCompatToPlatform() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        Bundle extras = new Bundle();
+        extras.putString("sampleExtrasKey", "sampleExtrasValue");
+        ContentInfoCompat contentInfo = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
+                .setFlags(FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(extras)
+                .build();
+
+        // Verify that retrieving the platform object returns a shallow copy.
+        ContentInfo platContentInfo = contentInfo.toContentInfo();
+        assertThat(platContentInfo.getClip()).isSameInstanceAs(contentInfo.getClip());
+        assertThat(platContentInfo.getSource()).isEqualTo(contentInfo.getSource());
+        assertThat(platContentInfo.getFlags()).isEqualTo(contentInfo.getFlags());
+        assertThat(platContentInfo.getLinkUri()).isSameInstanceAs(contentInfo.getLinkUri());
+        assertThat(platContentInfo.getExtras()).isSameInstanceAs(contentInfo.getExtras());
+
+        // Verify that retrieving the platform object multiple times returns the same instance.
+        ContentInfo platContentInfo2 = contentInfo.toContentInfo();
+        assertThat(platContentInfo2).isSameInstanceAs(platContentInfo);
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testPlatformToCompat() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        Bundle extras = new Bundle();
+        extras.putString("sampleExtrasKey", "sampleExtrasValue");
+        ContentInfo platContentInfo = new ContentInfo.Builder(clip, ContentInfo.SOURCE_CLIPBOARD)
+                .setFlags(ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(extras)
+                .build();
+
+        // Verify that converting to the compat object returns a shallow copy.
+        ContentInfoCompat contentInfo = ContentInfoCompat.toContentInfoCompat(platContentInfo);
+        assertThat(contentInfo.getClip()).isSameInstanceAs(platContentInfo.getClip());
+        assertThat(contentInfo.getSource()).isEqualTo(platContentInfo.getSource());
+        assertThat(contentInfo.getFlags()).isEqualTo(platContentInfo.getFlags());
+        assertThat(contentInfo.getLinkUri()).isSameInstanceAs(platContentInfo.getLinkUri());
+        assertThat(contentInfo.getExtras()).isSameInstanceAs(platContentInfo.getExtras());
+
+        // Verify that converting to the compat object multiple times returns a new instance each
+        // time.
+        ContentInfoCompat contentInfo2 = ContentInfoCompat.toContentInfoCompat(platContentInfo);
+        assertThat(contentInfo2).isNotSameInstanceAs(contentInfo);
+
+        // Verify that converting back from the compat object returns the original platform
+        // instance.
+        assertThat(contentInfo.toContentInfo()).isSameInstanceAs(platContentInfo);
+        assertThat(contentInfo2.toContentInfo()).isSameInstanceAs(platContentInfo);
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testPartitionPlatformContentInfo_multipleItems() throws Exception {
+        Uri sampleUri = Uri.parse("content://com.example/path");
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        clip.addItem(new ClipData.Item("Hi", "<b>Salut</b>"));
+        clip.addItem(new ClipData.Item(sampleUri));
+        ContentInfo payload = new ContentInfo.Builder(clip, ContentInfo.SOURCE_CLIPBOARD)
+                .setFlags(ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        // Test splitting when some items match and some don't.
+        Pair<ContentInfo, ContentInfo> split;
+        split = ContentInfoCompat.partition(payload,
+                new java.util.function.Predicate<ClipData.Item>() {
+                    @Override
+                    public boolean test(ClipData.Item item) {
+                        return item.getUri() != null;
+                    }
+                });
+        assertThat(split.first.getClip().getItemCount()).isEqualTo(1);
+        assertThat(split.second.getClip().getItemCount()).isEqualTo(2);
+        assertThat(split.first.getClip().getItemAt(0).getUri()).isEqualTo(sampleUri);
+        assertThat(split.first.getClip().getDescription()).isNotSameInstanceAs(
+                payload.getClip().getDescription());
+        assertThat(split.second.getClip().getDescription()).isNotSameInstanceAs(
+                payload.getClip().getDescription());
+        assertThat(split.first.getSource()).isEqualTo(ContentInfo.SOURCE_CLIPBOARD);
+        assertThat(split.first.getLinkUri()).isNotNull();
+        assertThat(split.first.getExtras()).isNotNull();
+        assertThat(split.second.getSource()).isEqualTo(ContentInfo.SOURCE_CLIPBOARD);
+        assertThat(split.second.getLinkUri()).isNotNull();
+        assertThat(split.second.getExtras()).isNotNull();
+
+        // Test splitting when none of the items match.
+        split = ContentInfoCompat.partition(payload,
+                new java.util.function.Predicate<ClipData.Item>() {
+                    @Override
+                    public boolean test(ClipData.Item item) {
+                        return false;
+                    }
+                });
+        assertThat(split.first).isNull();
+        assertThat(split.second).isSameInstanceAs(payload);
+
+        // Test splitting when all of the items match.
+        split = ContentInfoCompat.partition(payload,
+                new java.util.function.Predicate<ClipData.Item>() {
+                    @Override
+                    public boolean test(ClipData.Item item) {
+                        return true;
+                    }
+                });
+        assertThat(split.first).isSameInstanceAs(payload);
+        assertThat(split.second).isNull();
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    public void testPartitionPlatformContentInfo_singleItem() throws Exception {
+        ClipData clip = ClipData.newPlainText("", "Hello");
+        ContentInfo payload = new ContentInfo.Builder(clip, ContentInfo.SOURCE_CLIPBOARD)
+                .setFlags(ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT)
+                .setLinkUri(Uri.parse("http://example.com"))
+                .setExtras(new Bundle())
+                .build();
+
+        Pair<ContentInfo, ContentInfo> split;
+        split = ContentInfoCompat.partition(payload,
+                new java.util.function.Predicate<ClipData.Item>() {
+                    @Override
+                    public boolean test(ClipData.Item item) {
+                        return false;
+                    }
+                });
+        assertThat(split.first).isNull();
+        assertThat(split.second).isSameInstanceAs(payload);
+
+        split = ContentInfoCompat.partition(payload,
+                new java.util.function.Predicate<ClipData.Item>() {
+                    @Override
+                    public boolean test(ClipData.Item item) {
+                        return true;
+                    }
+                });
+        assertThat(split.first).isSameInstanceAs(payload);
+        assertThat(split.second).isNull();
+    }
 }
diff --git a/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java
new file mode 100644
index 0000000..c4a7136
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2021 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 androidx.core.widget;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.support.v4.BaseTestActivity;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.EdgeEffect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.test.R;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EdgeEffectCompatTest extends
+        BaseInstrumentationTestCase<EdgeEffectCompatTest.EdgeEffectCompatTestActivity> {
+    private ViewWithEdgeEffect mView;
+    private EdgeEffect mEdgeEffect;
+
+    public EdgeEffectCompatTest() {
+        super(EdgeEffectCompatTestActivity.class);
+    }
+
+    @Before
+    public void setUp() {
+        Activity activity = mActivityTestRule.getActivity();
+        mView = activity.findViewById(R.id.edgeEffectView);
+        mEdgeEffect = mView.mEdgeEffect;
+    }
+
+    // TODO(b/181171227): Change to R
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    public void distanceApi30() {
+        assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+        assertEquals(1f, EdgeEffectCompat.onPullDistance(mEdgeEffect, 1, 0.5f), 0f);
+        assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+    }
+
+    // TODO(b/181171227): Change to S
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    public void distanceApi31() {
+        // TODO(b/181171227): Remove this condition
+        if (isSOrHigher()) {
+            assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+            assertEquals(1f, EdgeEffectCompat.onPullDistance(mEdgeEffect, 1, 0.5f), 0f);
+            assertEquals(1, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+            assertEquals(-1f, EdgeEffectCompat.onPullDistance(mEdgeEffect, -1.5f, 0.5f), 0f);
+            assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+        } else {
+            distanceApi30();
+        }
+    }
+
+    // TODO(b/181171227): Remove this.
+    private static boolean isSOrHigher() {
+        int sdk = Build.VERSION.SDK_INT;
+        return sdk > Build.VERSION_CODES.R
+                || (sdk == Build.VERSION_CODES.R && Build.VERSION.PREVIEW_SDK_INT != 0);
+    }
+
+    public static class EdgeEffectCompatTestActivity extends BaseTestActivity {
+        @Override
+        protected int getContentViewLayoutResId() {
+            return R.layout.edge_effect_compat;
+        }
+    }
+
+    public static class ViewWithEdgeEffect extends View {
+        public EdgeEffect mEdgeEffect;
+
+        public ViewWithEdgeEffect(Context context) {
+            super(context);
+            initEdgeEffect(context, null);
+        }
+
+        public ViewWithEdgeEffect(Context context, AttributeSet attrs) {
+            super(context, attrs);
+            initEdgeEffect(context, attrs);
+        }
+
+        public ViewWithEdgeEffect(Context context, AttributeSet attrs, int defStyleAttr) {
+            super(context, attrs, defStyleAttr);
+            initEdgeEffect(context, attrs);
+        }
+
+        @RequiresApi(21)
+        @SuppressWarnings("unused")
+        public ViewWithEdgeEffect(Context context, AttributeSet attrs, int defStyleAttr,
+                int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+            initEdgeEffect(context, attrs);
+        }
+
+        private void initEdgeEffect(@NonNull Context context, @Nullable AttributeSet attrs) {
+            mEdgeEffect = EdgeEffectCompat.create(context, attrs);
+        }
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
index 1391695..d01a587 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
@@ -83,6 +83,7 @@
         mNestedScrollView = new NestedScrollView(ApplicationProvider.getApplicationContext());
         mNestedScrollView.setMinimumWidth(1000);
         mNestedScrollView.setMinimumHeight(1000);
+        mNestedScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
 
         mParentSpy = Mockito.spy(
                 new NestedScrollingSpyView(ApplicationProvider.getApplicationContext()));
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
index a6b9e414..90d984a 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
@@ -18,14 +18,20 @@
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
 import android.os.Parcelable;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.View;
+import android.widget.EdgeEffect;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -303,6 +309,127 @@
         assertThat(mNestedScrollView.getScrollY(), is(100));
     }
 
+    @Test
+    public void testTopEdgeEffectReversal() {
+        setup(200);
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+        swipeDown(false);
+        assertEquals(0, mNestedScrollView.getScrollY());
+        swipeUp(true);
+        if (BuildCompat.isAtLeastS()) {
+            // This should just reverse the overscroll effect
+            assertEquals(0, mNestedScrollView.getScrollY());
+        } else {
+            // Can't catch the overscroll effect for R and earlier
+            assertNotEquals(0, mNestedScrollView.getScrollY());
+        }
+    }
+
+    @Test
+    public void testBottomEdgeEffectReversal() {
+        setup(200);
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+        int scrollRange = mNestedScrollView.getScrollRange();
+        mNestedScrollView.scrollTo(0, scrollRange);
+        assertEquals(scrollRange, mNestedScrollView.getScrollY());
+        swipeUp(false);
+        assertEquals(scrollRange, mNestedScrollView.getScrollY());
+        swipeDown(true);
+        if (BuildCompat.isAtLeastS()) {
+            // This should just reverse the overscroll effect
+            assertEquals(scrollRange, mNestedScrollView.getScrollY());
+        } else {
+            // Can't catch the overscroll effect for R and earlier
+            assertNotEquals(scrollRange, mNestedScrollView.getScrollY());
+        }
+    }
+
+    @Test
+    public void testFlingWhileStretchedAtTop() {
+        setup(200);
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+        CaptureOnAbsorbEdgeEffect edgeEffect =
+                new CaptureOnAbsorbEdgeEffect(mNestedScrollView.getContext());
+        mNestedScrollView.mEdgeGlowTop = edgeEffect;
+        flingDown();
+        assertTrue(edgeEffect.pullDistance > 0);
+
+        if (BuildCompat.isAtLeastS()) {
+            assertTrue(edgeEffect.absorbVelocity > 0);
+        } else {
+            assertEquals(0, edgeEffect.absorbVelocity);
+            flingUp();
+            assertNotEquals(0, mNestedScrollView.getScrollY());
+        }
+    }
+
+    @Test
+    public void testFlingWhileStretchedAtBottom() {
+        setup(200);
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+        CaptureOnAbsorbEdgeEffect edgeEffect =
+                new CaptureOnAbsorbEdgeEffect(mNestedScrollView.getContext());
+        mNestedScrollView.mEdgeGlowBottom = edgeEffect;
+
+        int scrollRange = mNestedScrollView.getScrollRange();
+        mNestedScrollView.scrollTo(0, scrollRange);
+        assertEquals(scrollRange, mNestedScrollView.getScrollY());
+        flingUp();
+        assertTrue(edgeEffect.pullDistance > 0);
+        assertEquals(scrollRange, mNestedScrollView.getScrollY());
+
+        if (BuildCompat.isAtLeastS()) {
+            assertTrue(edgeEffect.absorbVelocity > 0);
+        } else {
+            assertEquals(0, edgeEffect.absorbVelocity);
+            flingDown();
+            assertNotEquals(scrollRange, mNestedScrollView.getScrollY());
+        }
+    }
+
+    private void swipeDown(boolean shortSwipe) {
+        float endY = shortSwipe ? mNestedScrollView.getHeight() / 2f :
+                mNestedScrollView.getHeight() - 1;
+        swipe(0, endY);
+    }
+
+    private void swipeUp(boolean shortSwipe) {
+        float endY = shortSwipe ? mNestedScrollView.getHeight() / 2f : 0;
+        swipe(mNestedScrollView.getHeight() - 1, endY);
+    }
+
+    private void swipe(float startY, float endY) {
+        float x = mNestedScrollView.getWidth() / 2f;
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, startY, 0);
+        mNestedScrollView.dispatchTouchEvent(down);
+        MotionEvent move = MotionEvent.obtain(0, 10, MotionEvent.ACTION_MOVE, x, endY, 0);
+        mNestedScrollView.dispatchTouchEvent(move);
+        MotionEvent up = MotionEvent.obtain(0, 1000, MotionEvent.ACTION_UP, x, endY, 0);
+        mNestedScrollView.dispatchTouchEvent(up);
+    }
+
+    private void flingDown() {
+        fling(0, mNestedScrollView.getHeight() - 1);
+    }
+
+    private void flingUp() {
+        fling(mNestedScrollView.getHeight() - 1, 0);
+    }
+
+    private void fling(float startY, float endY) {
+        float x = mNestedScrollView.getWidth() / 2f;
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, startY, 0);
+        mNestedScrollView.dispatchTouchEvent(down);
+        MotionEvent move = MotionEvent.obtain(0, 10, MotionEvent.ACTION_MOVE, x, endY, 0);
+        mNestedScrollView.dispatchTouchEvent(move);
+        MotionEvent up = MotionEvent.obtain(0, 11, MotionEvent.ACTION_UP, x, endY, 0);
+        mNestedScrollView.dispatchTouchEvent(up);
+    }
+
     private void setup(int childHeight) {
         Context context = ApplicationProvider.getApplicationContext();
 
@@ -339,4 +466,36 @@
         measure(height);
         mNestedScrollView.layout(0, 0, 100, height);
     }
+
+    private static class CaptureOnAbsorbEdgeEffect extends EdgeEffect {
+        public int absorbVelocity;
+        public float pullDistance;
+
+        CaptureOnAbsorbEdgeEffect(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onPull(float deltaDistance) {
+            pullDistance += deltaDistance;
+            super.onPull(deltaDistance);
+        }
+
+        @Override
+        public void onPull(float deltaDistance, float displacement) {
+            pullDistance += deltaDistance;
+            super.onPull(deltaDistance, displacement);
+        }
+
+        @Override
+        public void onAbsorb(int velocity) {
+            absorbVelocity = velocity;
+            super.onAbsorb(velocity);
+        }
+
+        @Override
+        public void onRelease() {
+            super.onRelease();
+        }
+    }
 }
diff --git a/core/core/src/androidTest/res/color/color_state_list_android_lstar.xml b/core/core/src/androidTest/res/color/color_state_list_android_lstar.xml
new file mode 100644
index 0000000..541d0de
--- /dev/null
+++ b/core/core/src/androidTest/res/color/color_state_list_android_lstar.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  ~ Copyright (C) 2021 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:alpha="0.5" android:color="#FFA6C839" android:lStar="50" />
+</selector>
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/color/color_state_list_lstar.xml b/core/core/src/androidTest/res/color/color_state_list_lstar.xml
new file mode 100644
index 0000000..f9de313
--- /dev/null
+++ b/core/core/src/androidTest/res/color/color_state_list_lstar.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  ~ Copyright (C) 2021 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item android:alpha="0.5" android:color="#FFA6C839" app:lStar="50" />
+</selector>
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/layout/edge_effect_compat.xml b/core/core/src/androidTest/res/layout/edge_effect_compat.xml
new file mode 100644
index 0000000..fda0658
--- /dev/null
+++ b/core/core/src/androidTest/res/layout/edge_effect_compat.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<view
+    class="androidx.core.widget.EdgeEffectCompatTest$ViewWithEdgeEffect"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/edgeEffectView"
+    android:layout_width="100px"
+    android:layout_height="100px" />
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/layout/nested_scroll_view_stretch.xml b/core/core/src/androidTest/res/layout/nested_scroll_view_stretch.xml
new file mode 100644
index 0000000..c18e4ad
--- /dev/null
+++ b/core/core/src/androidTest/res/layout/nested_scroll_view_stretch.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<androidx.core.widget.NestedScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/nestedScrollView"
+    android:layout_width="100px"
+    android:layout_height="100px" />
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/app/ActivityCompat.java b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
index 5a05109..945b9d2 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
@@ -30,6 +30,7 @@
 import android.os.Looper;
 import android.os.Parcelable;
 import android.text.TextUtils;
+import android.view.Display;
 import android.view.DragEvent;
 import android.view.View;
 
@@ -41,6 +42,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.LocusIdCompat;
+import androidx.core.os.BuildCompat;
 import androidx.core.view.DragAndDropPermissionsCompat;
 
 import java.util.Arrays;
@@ -556,6 +558,42 @@
     }
 
     /**
+     * Indicates whether this activity is launched from a bubble. A bubble is a floating shortcut
+     * on the screen that expands to show an activity.
+     *
+     * If your activity can be used normally or as a bubble, you might use this method to check
+     * if the activity is bubbled to modify any behaviour that might be different between the
+     * normal activity and the bubbled activity. For example, if you normally cancel the
+     * notification associated with the activity when you open the activity, you might not want to
+     * do that when you're bubbled as that would remove the bubble.
+     *
+     * @return {@code true} if the activity is launched from a bubble.
+     *
+     * @see NotificationCompat.Builder#setBubbleMetadata(NotificationCompat.BubbleMetadata)
+     * @see NotificationCompat.BubbleMetadata.Builder#Builder(String)
+     *
+     * Compatibility behavior:
+     * <ul>
+     *     <li>API 31 and above, this method matches platform behavior</li>
+     *     <li>API 29, 30, this method checks the window display ID</li>
+     *     <li>API 28 and earlier, this method is a no-op</li>
+     * </ul>
+     */
+    public static boolean isLaunchedFromBubble(@NonNull Activity activity) {
+        if (BuildCompat.isAtLeastS()) {
+            return Api31Impl.isLaunchedFromBubble(activity);
+        } else if (Build.VERSION.SDK_INT == 30) {
+            return activity.getDisplay() != null
+                    && activity.getDisplay().getDisplayId() != Display.DEFAULT_DISPLAY;
+        } else if (Build.VERSION.SDK_INT == 29) {
+            return activity.getWindowManager().getDefaultDisplay() != null
+                    && activity.getWindowManager().getDefaultDisplay().getDisplayId()
+                    != Display.DEFAULT_DISPLAY;
+        }
+        return false;
+    }
+
+    /**
      * Create {@link DragAndDropPermissionsCompat} object bound to this activity and controlling
      * the access permissions for content URIs associated with the {@link android.view.DragEvent}.
      * @param dragEvent Drag event to request permission for
@@ -715,4 +753,19 @@
             activity.setLocusContext(locusId == null ? null : locusId.toLocusId(), bundle);
         }
     }
+
+    @RequiresApi(31)
+    static class Api31Impl  {
+
+      /**
+       * This class should not be instantiated.
+       */
+        private Api31Impl() {
+            // Not intended for instantiation.
+        }
+
+        static boolean isLaunchedFromBubble(@NonNull final Activity activity)  {
+            return activity.isLaunchedFromBubble();
+        }
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
index 9ca7d09..c8b4123 100644
--- a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
@@ -16,6 +16,7 @@
 
 package androidx.core.app;
 
+import android.annotation.SuppressLint;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.os.Build;
@@ -54,6 +55,7 @@
      * @see android.content.Context#registerReceiver
      * @see android.content.Intent#filterEquals
      */
+    @SuppressLint("MissingPermission")
     public static void setAlarmClock(@NonNull AlarmManager alarmManager, long triggerTime,
             @NonNull PendingIntent showIntent, @NonNull PendingIntent operation) {
         if (Build.VERSION.SDK_INT >= 21) {
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 25389a0..ae4f28e 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -795,6 +795,56 @@
      */
     public static final String GROUP_KEY_SILENT = "silent";
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @IntDef({FOREGROUND_SERVICE_DEFAULT,
+            FOREGROUND_SERVICE_IMMEDIATE,
+            FOREGROUND_SERVICE_DEFERRED})
+    public @interface ServiceNotificationBehavior {}
+
+    /**
+     * Constant for {@link Builder#setForegroundServiceBehavior(int)}. In Android 12 or later,
+     * if the Notification associated with starting a foreground service has been
+     * built using setForegroundServiceBehavior() with this behavior, display of
+     * the notification will often be suppressed for a short time to avoid visual
+     * disturbances to the user.
+     *
+     * @see NotificationCompat.Builder#setForegroundServiceBehavior(int)
+     * @see #FOREGROUND_SERVICE_IMMEDIATE
+     * @see #FOREGROUND_SERVICE_DEFERRED
+     */
+    public static final int FOREGROUND_SERVICE_DEFAULT =
+            Notification.FOREGROUND_SERVICE_DEFAULT;
+
+    /**
+     * Constant for {@link Builder#setForegroundServiceBehavior(int)}. In Android 12 or later,
+     * if the Notification associated with starting a foreground service has been
+     * built using setForegroundServiceBehavior() with this behavior, display of
+     * the notification will be immediate even if the default behavior would be
+     * to defer visibility for a short time.
+     *
+     * @see NotificationCompat.Builder#setForegroundServiceBehavior(int)
+     * @see #FOREGROUND_SERVICE_DEFAULT
+     * @see #FOREGROUND_SERVICE_DEFERRED
+     */
+    public static final int FOREGROUND_SERVICE_IMMEDIATE =
+            Notification.FOREGROUND_SERVICE_IMMEDIATE;
+
+    /**
+     * Constant for {@link Builder#setForegroundServiceBehavior(int)}. In Android 12 or later,
+     * if the Notification associated with starting a foreground service has been
+     * built using setForegroundServiceBehavior() with this behavior, display of
+     * the notification will usually be suppressed for a short time to avoid visual
+     * disturbances to the user.
+     *
+     * @see NotificationCompat.Builder#setForegroundServiceBehavior(int)
+     * @see #FOREGROUND_SERVICE_DEFAULT
+     * @see #FOREGROUND_SERVICE_IMMEDIATE
+     */
+    public static final int FOREGROUND_SERVICE_DEFERRED =
+            Notification.FOREGROUND_SERVICE_DEFERRED;
+
     /**
      * Builder class for {@link NotificationCompat} objects.  Allows easier control over
      * all the flags, as well as help constructing the typical notification layouts.
@@ -884,6 +934,7 @@
         LocusIdCompat mLocusId;
         long mTimeout;
         @GroupAlertBehavior int mGroupAlertBehavior = GROUP_ALERT_ALL;
+        @ServiceNotificationBehavior int mFgsDeferBehavior = FOREGROUND_SERVICE_DEFAULT;
         boolean mAllowSystemGeneratedContextualActions;
         BubbleMetadata mBubbleMetadata;
         Notification mNotification = new Notification();
@@ -2297,6 +2348,31 @@
         }
 
         /**
+         * Specify a desired visibility policy for a Notification associated with a
+         * foreground service.  The default value is {@link #FOREGROUND_SERVICE_DEFAULT},
+         * meaning the system can choose to defer visibility of the notification for
+         * a short time after the service is started.  Pass
+         * {@link NotificationCompat#FOREGROUND_SERVICE_IMMEDIATE FOREGROUND_SERVICE_IMMEDIATE}
+         * to this method in order to guarantee that visibility is never deferred.  Pass
+         * {@link NotificationCompat#FOREGROUND_SERVICE_DEFERRED FOREGROUND_SERVICE_DEFERRED}
+         * to request that visibility is deferred whenever possible.
+         *
+         * <p class="note">Note that deferred visibility is not guaranteed.  There
+         * may be some circumstances under which the system will show the foreground
+         * service's associated Notification immediately even when the app has used
+         * this method to explicitly request deferred display.</p>
+         *
+         * This method has no effect when running on versions prior to
+          * {@link android.os.Build.VERSION_CODES#S}.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder") // no underlying getter in platform API
+        @NonNull
+        public Builder setForegroundServiceBehavior(@ServiceNotificationBehavior int behavior) {
+            mFgsDeferBehavior = behavior;
+            return this;
+        }
+
+        /**
          * Sets the {@link BubbleMetadata} that will be used to display app content in a floating
          * window over the existing foreground activity.
          *
@@ -2399,6 +2475,16 @@
         }
 
         /**
+         * @return the foreground service behavior defined for the notification
+         *
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        public int getForegroundServiceBehavior() {
+            return mFgsDeferBehavior;
+        }
+
+        /**
          * @return the color of the notification
          *
          * @hide
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index 00b1660..827b9bf 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -36,6 +36,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.collection.ArraySet;
 import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.os.BuildCompat;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -247,6 +248,11 @@
                 mBuilder.setLocusId(b.mLocusId.toLocusId());
             }
         }
+        if (BuildCompat.isAtLeastS()) {
+            if (b.mFgsDeferBehavior != NotificationCompat.FOREGROUND_SERVICE_DEFAULT) {
+                mBuilder.setForegroundServiceBehavior(b.mFgsDeferBehavior);
+            }
+        }
 
         if (b.mSilent) {
             if (mBuilderCompat.mGroupSummary) {
diff --git a/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java b/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
index 2015927..a919768 100644
--- a/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
@@ -36,6 +36,9 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.XmlRes;
 import androidx.core.R;
+import androidx.core.graphics.ColorUtils;
+import androidx.core.math.MathUtils;
+import androidx.core.os.BuildCompat;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -165,6 +168,14 @@
                 alphaMod = a.getFloat(R.styleable.ColorStateListItem_alpha, alphaMod);
             }
 
+            final float lStar;
+            if (BuildCompat.isAtLeastS()
+                    && a.hasValue(R.styleable.ColorStateListItem_android_lStar)) {
+                lStar = a.getFloat(R.styleable.ColorStateListItem_android_lStar, -1.0f);
+            } else {
+                lStar = a.getFloat(R.styleable.ColorStateListItem_lStar, -1.0f);
+            }
+
             a.recycle();
 
             // Parse all unrecognized attributes as state specifiers.
@@ -173,8 +184,10 @@
             int[] stateSpec = new int[numAttrs];
             for (int i = 0; i < numAttrs; i++) {
                 final int stateResId = attrs.getAttributeNameResource(i);
-                if (stateResId != android.R.attr.color && stateResId != android.R.attr.alpha
-                        && stateResId != R.attr.alpha) {
+                if (stateResId != android.R.attr.color
+                        && stateResId != android.R.attr.alpha
+                        && stateResId != R.attr.alpha
+                        && stateResId != R.attr.lStar) {
                     // Unrecognized attribute, add to state set
                     stateSpec[j++] = attrs.getAttributeBooleanValue(i, false)
                             ? stateResId : -stateResId;
@@ -182,10 +195,10 @@
             }
             stateSpec = StateSet.trimStateSet(stateSpec, j);
 
-            // Apply alpha modulation. If we couldn't resolve the color or
+            // Apply alpha and luminance modulation. If we couldn't resolve the color or
             // alpha yet, the default values leave us enough information to
             // modulate again during applyTheme().
-            final int color = modulateColorAlpha(baseColor, alphaMod);
+            final int color = modulateColorAlpha(baseColor, alphaMod, lStar);
 
             colorList = GrowingArrayUtils.append(colorList, listSize, color);
             stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec);
@@ -225,8 +238,22 @@
 
     @ColorInt
     private static int modulateColorAlpha(@ColorInt int color,
-            @FloatRange(from = 0f, to = 1f) float alphaMod) {
-        int alpha = Math.round(Color.alpha(color) * alphaMod);
-        return (color & 0x00ffffff) | (alpha << 24);
+            @FloatRange(from = 0f, to = 1f) float alphaMod,
+            @FloatRange(from = 0f, to = 100f) float lStar) {
+        final boolean validLStar = lStar >= 0.0f && lStar <= 100.0f;
+        if (alphaMod == 1.0f && !validLStar) {
+            return color;
+        }
+
+        final int baseAlpha = Color.alpha(color);
+        final int alpha = MathUtils.clamp((int) (baseAlpha * alphaMod + 0.5f), 0, 255);
+
+        if (validLStar) {
+            final double[] labColor = new double[3];
+            ColorUtils.colorToLAB(color, labColor);
+            color = ColorUtils.LABToColor(lStar, labColor[1], labColor[2]);
+        }
+
+        return (color & 0xFFFFFF) | (alpha << 24);
     }
 }
diff --git a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
index ff21626c..f27583d 100644
--- a/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
+++ b/core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
@@ -190,12 +190,10 @@
     @SuppressWarnings("deprecation")
     public static ColorStateList getColorStateList(@NonNull Resources res, @ColorRes int id,
             @Nullable Theme theme) throws NotFoundException {
-        if (SDK_INT >= 23) {
-            // On M+ we can use the framework
-            return res.getColorStateList(id, theme);
-        }
+        // We explicitly do not attempt to use the platform Resources impl on S+
+        // in case the CSL is using only app:lStar
 
-        // Before that, we'll try handle it ourselves
+        // First, try and handle the inflation ourselves
         ColorStateListCacheKey key = new ColorStateListCacheKey(res, theme);
         ColorStateList csl = getCachedColorStateList(key, id);
         if (csl != null) {
diff --git a/core/core/src/main/java/androidx/core/location/LocationListenerCompat.java b/core/core/src/main/java/androidx/core/location/LocationListenerCompat.java
new file mode 100644
index 0000000..d626af1
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/location/LocationListenerCompat.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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 androidx.core.location;
+
+import android.location.LocationListener;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A version of {@link LocationListener} suitable for use on all API levels.
+ */
+public interface LocationListenerCompat extends LocationListener {
+
+    @Override
+    default void onStatusChanged(@NonNull String provider, int status, @Nullable Bundle extras) {}
+
+    @Override
+    default void onProviderEnabled(@NonNull String provider) {}
+
+    @Override
+    default void onProviderDisabled(@NonNull String provider) {}
+}
diff --git a/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java b/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
index 61a7836..c450492 100644
--- a/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
@@ -32,6 +32,7 @@
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
+import android.location.LocationRequest;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
@@ -52,10 +53,17 @@
 import androidx.core.os.CancellationSignal;
 import androidx.core.os.ExecutorCompat;
 import androidx.core.util.Consumer;
+import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
+import java.lang.ref.WeakReference;
 import java.lang.reflect.Field;
-import java.util.concurrent.Callable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.WeakHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.FutureTask;
@@ -65,6 +73,7 @@
 /**
  * Helper for accessing features in {@link LocationManager}.
  */
+@SuppressWarnings("deprecation")
 public final class LocationManagerCompat {
 
     private static final long GET_CURRENT_LOCATION_TIMEOUT_MS = 30 * 1000;
@@ -72,6 +81,8 @@
     private static final long PRE_N_LOOPER_TIMEOUT_S = 5;
 
     private static Field sContextField;
+    private static Method sRequestLocationUpdatesExecutorMethod;
+    private static Method sRequestLocationUpdatesLooperMethod;
 
     /**
      * Returns the current enabled/disabled state of location.
@@ -84,6 +95,7 @@
      *
      * @return {@code true} if location is enabled or {@code false} if location is disabled
      */
+    @SuppressWarnings("JavaReflectionMemberAccess")
     public static boolean isLocationEnabled(@NonNull LocationManager locationManager) {
         if (VERSION.SDK_INT >= 28) {
             return Api28Impl.isLocationEnabled(locationManager);
@@ -122,6 +134,33 @@
     }
 
     /**
+     * Returns true if the given location provider exists on this device, irrespective of whether
+     * it is currently enabled or not. If called on Android Q and below for the
+     * {@link LocationManager#FUSED_PROVIDER}, this method may return incorrect results if the
+     * client does not hold at least the {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
+     * permission.
+     */
+    public static boolean hasProvider(@NonNull LocationManager locationManager,
+            @NonNull String provider) {
+        if (VERSION.SDK_INT >= 31) {
+            return Api31Impl.hasProvider(locationManager, provider);
+        }
+
+        // will not work for the FUSED provider by default
+        if (locationManager.getAllProviders().contains(provider)) {
+            return true;
+        }
+
+        try {
+            // Q and below have pointless location permission requirements when using getProvider()
+            return locationManager.getProvider(provider) != null;
+        } catch (SecurityException ignored) {
+        }
+
+        return false;
+    }
+
+    /**
      * Asynchronously returns a single current location fix from the given provider. This may
      * activate sensors in order to compute a new location. The given callback will be invoked once
      * and only once, either with a valid location or with a null location if the provider was
@@ -146,43 +185,182 @@
         if (VERSION.SDK_INT >= 30) {
             Api30Impl.getCurrentLocation(locationManager, provider, cancellationSignal, executor,
                     consumer);
-        } else {
-            if (cancellationSignal != null) {
-                cancellationSignal.throwIfCanceled();
-            }
+            return;
+        }
 
-            final Location location = locationManager.getLastKnownLocation(provider);
-            if (location != null) {
-                long locationAgeMs =
-                        SystemClock.elapsedRealtime() - getElapsedRealtimeMillis(location);
-                if (locationAgeMs < MAX_CURRENT_LOCATION_AGE_MS) {
-                    executor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            consumer.accept(location);
-                        }
-                    });
-                    return;
+        if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
+        }
+
+        final Location location = locationManager.getLastKnownLocation(provider);
+        if (location != null) {
+            long locationAgeMs =
+                    SystemClock.elapsedRealtime() - getElapsedRealtimeMillis(location);
+            if (locationAgeMs < MAX_CURRENT_LOCATION_AGE_MS) {
+                executor.execute(() -> consumer.accept(location));
+                return;
+            }
+        }
+
+        final CancellableLocationListener listener =
+                new CancellableLocationListener(locationManager, executor, consumer);
+        locationManager.requestLocationUpdates(provider, 0, 0, listener,
+                Looper.getMainLooper());
+
+        if (cancellationSignal != null) {
+            cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
+                @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
+                @Override
+                public void onCancel() {
+                    listener.cancel();
+                }
+            });
+        }
+
+        listener.startTimeout(GET_CURRENT_LOCATION_TIMEOUT_MS);
+    }
+
+    @GuardedBy("sLocationListeners")
+    static final WeakHashMap<LocationListener,
+            List<WeakReference<LocationListenerTransport>>> sLocationListeners =
+            new WeakHashMap<>();
+
+    /**
+     * Register for location updates from the specified provider, using a
+     * {@link LocationRequestCompat}, and a callback on the specified {@link Executor}.
+     *
+     * <p>See
+     * {@link LocationManager#requestLocationUpdates(String, LocationRequest, Executor,
+     * LocationListener)} for more information.
+     */
+    @SuppressWarnings("JavaReflectionMemberAccess")
+    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
+    public static void requestLocationUpdates(@NonNull LocationManager locationManager,
+            @NonNull String provider,
+            @NonNull LocationRequestCompat locationRequest,
+            @NonNull Executor executor,
+            @NonNull LocationListenerCompat listener) {
+        if (VERSION.SDK_INT >= 31) {
+            Api31Impl.requestLocationUpdates(locationManager, provider,
+                    locationRequest.toLocationRequest(), executor, listener);
+            return;
+        }
+
+        if (VERSION.SDK_INT >= 30) {
+            try {
+                if (sRequestLocationUpdatesExecutorMethod == null) {
+                    sRequestLocationUpdatesExecutorMethod = LocationManager.class.getDeclaredMethod(
+                            "requestLocationUpdates",
+                            LocationRequest.class, Executor.class, LocationListener.class);
+                    sRequestLocationUpdatesExecutorMethod.setAccessible(true);
+                }
+
+                sRequestLocationUpdatesExecutorMethod.invoke(locationManager,
+                        locationRequest.toLocationRequest(provider), executor, listener);
+                return;
+            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+                // ignored
+            }
+        }
+
+        LocationListenerTransport transport = new LocationListenerTransport(listener, executor);
+
+        if (VERSION.SDK_INT >= 19) {
+            try {
+                if (sRequestLocationUpdatesLooperMethod == null) {
+                    sRequestLocationUpdatesLooperMethod = LocationManager.class.getDeclaredMethod(
+                            "requestLocationUpdates",
+                            LocationRequest.class, LocationListener.class, Looper.class);
+                    sRequestLocationUpdatesLooperMethod.setAccessible(true);
+                }
+
+                synchronized (sLocationListeners) {
+                    sRequestLocationUpdatesLooperMethod.invoke(locationManager,
+                            locationRequest.toLocationRequest(provider), transport,
+                            Looper.getMainLooper());
+                    transport.register();
+                }
+                return;
+            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+                // ignored
+            }
+        }
+
+        synchronized (sLocationListeners) {
+            locationManager.requestLocationUpdates(provider, locationRequest.getIntervalMillis(),
+                    locationRequest.getMinUpdateDistanceMeters(), transport,
+                    Looper.getMainLooper());
+            transport.register();
+        }
+    }
+
+    /**
+     * Register for location updates from the specified provider, using a
+     * {@link LocationRequestCompat}, and a callback on the specified {@link Looper}.
+     *
+     * <p>See
+     * {@link LocationManager#requestLocationUpdates(String, LocationRequest, Executor,
+     * LocationListener)} for more information.
+     */
+    @SuppressWarnings("JavaReflectionMemberAccess")
+    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
+    public static void requestLocationUpdates(@NonNull LocationManager locationManager,
+            @NonNull String provider,
+            @NonNull LocationRequestCompat locationRequest,
+            @NonNull LocationListenerCompat listener,
+            @NonNull Looper looper) {
+        if (VERSION.SDK_INT >= 31) {
+            Api31Impl.requestLocationUpdates(locationManager, provider,
+                    locationRequest.toLocationRequest(),
+                    ExecutorCompat.create(new Handler(looper)), listener);
+            return;
+        }
+
+        if (VERSION.SDK_INT >= 19) {
+            try {
+                if (sRequestLocationUpdatesLooperMethod == null) {
+                    sRequestLocationUpdatesLooperMethod = LocationManager.class.getDeclaredMethod(
+                            "requestLocationUpdates",
+                            LocationRequest.class, LocationListener.class, Looper.class);
+                    sRequestLocationUpdatesLooperMethod.setAccessible(true);
+                }
+
+                sRequestLocationUpdatesLooperMethod.invoke(locationManager,
+                        locationRequest.toLocationRequest(provider), listener, looper);
+                return;
+            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+                // ignored
+            }
+        }
+
+        locationManager.requestLocationUpdates(provider, locationRequest.getIntervalMillis(),
+                locationRequest.getMinUpdateDistanceMeters(), listener, looper);
+    }
+
+    /**
+     * Removes all location updates for the specified {@link LocationListener}.
+     *
+     * <p>See {@link LocationManager#removeUpdates(LocationListener)} for more information.
+     */
+    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
+    public static void removeUpdates(@NonNull LocationManager locationManager,
+            @NonNull LocationListenerCompat listener) {
+        synchronized (sLocationListeners) {
+            List<WeakReference<LocationListenerTransport>> transports =
+                    sLocationListeners.remove(listener);
+            if (transports != null) {
+                for (WeakReference<LocationListenerTransport> reference : transports) {
+                    LocationListenerTransport transport = reference.get();
+                    if (transport != null && transport.unregister()) {
+                        locationManager.removeUpdates(transport);
+                    }
                 }
             }
-
-            final CancellableLocationListener listener =
-                    new CancellableLocationListener(locationManager, executor, consumer);
-            locationManager.requestLocationUpdates(provider, 0, 0, listener,
-                    Looper.getMainLooper());
-
-            if (cancellationSignal != null) {
-                cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
-                    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
-                    @Override
-                    public void onCancel() {
-                        listener.cancel();
-                    }
-                });
-            }
-
-            listener.startTimeout(GET_CURRENT_LOCATION_TIMEOUT_MS);
         }
+
+        // a given listener could have been registered both with an executor and a looper, so we
+        // need to remove all possible cases
+        locationManager.removeUpdates(listener);
     }
 
     /**
@@ -212,9 +390,12 @@
         }
     }
 
-    @GuardedBy("sGnssStatusListeners")
-    private static final SimpleArrayMap<Object, Object> sGnssStatusListeners =
-            new SimpleArrayMap<>();
+    // allows lazy instantiation since most processes do not use GNSS APIs
+    private static class GnssLazyLoader {
+        @GuardedBy("sGnssStatusListeners")
+        static final SimpleArrayMap<Object, Object> sGnssStatusListeners =
+                new SimpleArrayMap<>();
+    }
 
     /**
      * Registers a platform agnostic {@link GnssStatusCompat.Callback}. See
@@ -272,14 +453,14 @@
     private static boolean registerGnssStatusCallback(final LocationManager locationManager,
             Handler baseHandler, Executor executor, GnssStatusCompat.Callback callback) {
         if (VERSION.SDK_INT >= VERSION_CODES.R) {
-            synchronized (sGnssStatusListeners) {
+            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                 GnssStatusTransport transport =
-                        (GnssStatusTransport) sGnssStatusListeners.get(callback);
+                        (GnssStatusTransport) GnssLazyLoader.sGnssStatusListeners.get(callback);
                 if (transport == null) {
                     transport = new GnssStatusTransport(callback);
                 }
                 if (locationManager.registerGnssStatusCallback(executor, transport)) {
-                    sGnssStatusListeners.put(callback, transport);
+                    GnssLazyLoader.sGnssStatusListeners.put(callback, transport);
                     return true;
                 } else {
                     return false;
@@ -287,9 +468,9 @@
             }
         } else if (VERSION.SDK_INT >= VERSION_CODES.N) {
             Preconditions.checkArgument(baseHandler != null);
-            synchronized (sGnssStatusListeners) {
+            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                 PreRGnssStatusTransport transport =
-                        (PreRGnssStatusTransport) sGnssStatusListeners.get(callback);
+                        (PreRGnssStatusTransport) GnssLazyLoader.sGnssStatusListeners.get(callback);
                 if (transport == null) {
                     transport = new PreRGnssStatusTransport(callback);
                 } else {
@@ -298,7 +479,7 @@
                 transport.register(executor);
 
                 if (locationManager.registerGnssStatusCallback(transport, baseHandler)) {
-                    sGnssStatusListeners.put(callback, transport);
+                    GnssLazyLoader.sGnssStatusListeners.put(callback, transport);
                     return true;
                 } else {
                     return false;
@@ -306,9 +487,9 @@
             }
         } else {
             Preconditions.checkArgument(baseHandler != null);
-            synchronized (sGnssStatusListeners) {
+            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                 GpsStatusTransport transport =
-                        (GpsStatusTransport) sGnssStatusListeners.get(callback);
+                        (GpsStatusTransport) GnssLazyLoader.sGnssStatusListeners.get(callback);
                 if (transport == null) {
                     transport = new GpsStatusTransport(locationManager, callback);
                 } else {
@@ -317,13 +498,8 @@
                 transport.register(executor);
 
                 final GpsStatusTransport myTransport = transport;
-                FutureTask<Boolean> task = new FutureTask<>(new Callable<Boolean>() {
-                    @RequiresPermission(ACCESS_FINE_LOCATION)
-                    @Override
-                    public Boolean call() {
-                        return locationManager.addGpsStatusListener(myTransport);
-                    }
-                });
+                FutureTask<Boolean> task = new FutureTask<>(
+                        () -> locationManager.addGpsStatusListener(myTransport));
 
                 if (Looper.myLooper() == baseHandler.getLooper()) {
                     task.run();
@@ -338,7 +514,7 @@
                     while (true) {
                         try {
                             if (task.get(remainingNanos, NANOSECONDS)) {
-                                sGnssStatusListeners.put(callback, myTransport);
+                                GnssLazyLoader.sGnssStatusListeners.put(callback, myTransport);
                                 return true;
                             } else {
                                 return false;
@@ -378,26 +554,27 @@
     public static void unregisterGnssStatusCallback(@NonNull LocationManager locationManager,
             @NonNull GnssStatusCompat.Callback callback) {
         if (VERSION.SDK_INT >= VERSION_CODES.R) {
-            synchronized (sGnssStatusListeners) {
+            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                 GnssStatusTransport transport =
-                        (GnssStatusTransport) sGnssStatusListeners.remove(callback);
+                        (GnssStatusTransport) GnssLazyLoader.sGnssStatusListeners.remove(callback);
                 if (transport != null) {
                     locationManager.unregisterGnssStatusCallback(transport);
                 }
             }
         } else if (VERSION.SDK_INT >= VERSION_CODES.N) {
-            synchronized (sGnssStatusListeners) {
+            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                 PreRGnssStatusTransport transport =
-                        (PreRGnssStatusTransport) sGnssStatusListeners.remove(callback);
+                        (PreRGnssStatusTransport) GnssLazyLoader.sGnssStatusListeners.remove(
+                                callback);
                 if (transport != null) {
                     transport.unregister();
                     locationManager.unregisterGnssStatusCallback(transport);
                 }
             }
         } else {
-            synchronized (sGnssStatusListeners) {
+            synchronized (GnssLazyLoader.sGnssStatusListeners) {
                 GpsStatusTransport transport =
-                        (GpsStatusTransport) sGnssStatusListeners.remove(callback);
+                        (GpsStatusTransport) GnssLazyLoader.sGnssStatusListeners.remove(callback);
                 if (transport != null) {
                     transport.unregister();
                     locationManager.removeGpsStatusListener(transport);
@@ -408,6 +585,151 @@
 
     private LocationManagerCompat() {}
 
+    private static class LocationListenerTransport implements LocationListener {
+
+        @Nullable volatile LocationListenerCompat mListener;
+        final Executor mExecutor;
+
+        LocationListenerTransport(@Nullable LocationListenerCompat listener, Executor executor) {
+            mListener = ObjectsCompat.requireNonNull(listener, "invalid null listener");
+            mExecutor = executor;
+        }
+
+        @GuardedBy("sLocationListeners")
+        public void register() {
+            List<WeakReference<LocationListenerTransport>> transports =
+                    sLocationListeners.get(mListener);
+            if (transports == null) {
+                transports = new ArrayList<>(1);
+                sLocationListeners.put(mListener, transports);
+            } else {
+                // clean unreferenced transports
+                if (VERSION.SDK_INT >= VERSION_CODES.N) {
+                    transports.removeIf(reference -> reference.get() == null);
+                } else {
+                    Iterator<WeakReference<LocationListenerTransport>> it = transports.iterator();
+                    while (it.hasNext()) {
+                        if (it.next().get() == null) {
+                            it.remove();
+                        }
+                    }
+                }
+            }
+
+            transports.add(new WeakReference<>(this));
+        }
+
+        @GuardedBy("sLocationListeners")
+        public boolean unregister() {
+            LocationListenerCompat listener = mListener;
+            if (listener == null) {
+                return false;
+            }
+            mListener = null;
+
+            List<WeakReference<LocationListenerTransport>> transports =
+                    sLocationListeners.get(listener);
+            if (transports != null) {
+                transports.removeIf(reference -> reference.get() == null);
+                if (transports.isEmpty()) {
+                    sLocationListeners.remove(listener);
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public void onLocationChanged(@NonNull Location location) {
+            final LocationListenerCompat listener = mListener;
+            if (listener == null) {
+                return;
+            }
+
+            mExecutor.execute(() -> {
+                if (mListener != listener) {
+                    return;
+                }
+                listener.onLocationChanged(location);
+            });
+        }
+
+        @Override
+        public void onLocationChanged(@NonNull List<Location> locations) {
+            final LocationListenerCompat listener = mListener;
+            if (listener == null) {
+                return;
+            }
+
+            mExecutor.execute(() -> {
+                if (mListener != listener) {
+                    return;
+                }
+                listener.onLocationChanged(locations);
+            });
+        }
+
+        @Override
+        public void onFlushComplete(int requestCode) {
+            final LocationListenerCompat listener = mListener;
+            if (listener == null) {
+                return;
+            }
+
+            mExecutor.execute(() -> {
+                if (mListener != listener) {
+                    return;
+                }
+                listener.onFlushComplete(requestCode);
+            });
+        }
+
+        @Override
+        public void onStatusChanged(String provider, int status, Bundle extras) {
+            final LocationListenerCompat listener = mListener;
+            if (listener == null) {
+                return;
+            }
+
+            mExecutor.execute(() -> {
+                if (mListener != listener) {
+                    return;
+                }
+                listener.onStatusChanged(provider, status, extras);
+            });
+        }
+
+        @Override
+        public void onProviderEnabled(@NonNull String provider) {
+            final LocationListenerCompat listener = mListener;
+            if (listener == null) {
+                return;
+            }
+
+            mExecutor.execute(() -> {
+                if (mListener != listener) {
+                    return;
+                }
+                listener.onProviderEnabled(provider);
+            });
+        }
+
+        @Override
+        public void onProviderDisabled(@NonNull String provider) {
+            final LocationListenerCompat listener = mListener;
+            if (listener == null) {
+                return;
+            }
+
+            mExecutor.execute(() -> {
+                if (mListener != listener) {
+                    return;
+                }
+                listener.onProviderDisabled(provider);
+            });
+        }
+    }
+
     @RequiresApi(VERSION_CODES.R)
     private static class GnssStatusTransport extends GnssStatus.Callback {
 
@@ -468,14 +790,11 @@
                 return;
             }
 
-            executor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    if (mExecutor != executor) {
-                        return;
-                    }
-                    mCallback.onStarted();
+            executor.execute(() -> {
+                if (mExecutor != executor) {
+                    return;
                 }
+                mCallback.onStarted();
             });
         }
 
@@ -486,14 +805,11 @@
                 return;
             }
 
-            executor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    if (mExecutor != executor) {
-                        return;
-                    }
-                    mCallback.onStopped();
+            executor.execute(() -> {
+                if (mExecutor != executor) {
+                    return;
                 }
+                mCallback.onStopped();
             });
         }
 
@@ -504,14 +820,11 @@
                 return;
             }
 
-            executor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    if (mExecutor != executor) {
-                        return;
-                    }
-                    mCallback.onFirstFix(ttffMillis);
+            executor.execute(() -> {
+                if (mExecutor != executor) {
+                    return;
                 }
+                mCallback.onFirstFix(ttffMillis);
             });
         }
 
@@ -522,14 +835,11 @@
                 return;
             }
 
-            executor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    if (mExecutor != executor) {
-                        return;
-                    }
-                    mCallback.onSatelliteStatusChanged(GnssStatusCompat.wrap(status));
+            executor.execute(() -> {
+                if (mExecutor != executor) {
+                    return;
                 }
+                mCallback.onSatelliteStatusChanged(GnssStatusCompat.wrap(status));
             });
         }
     }
@@ -569,39 +879,30 @@
 
             switch (event) {
                 case GpsStatus.GPS_EVENT_STARTED:
-                    executor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (mExecutor != executor) {
-                                return;
-                            }
-                            mCallback.onStarted();
+                    executor.execute(() -> {
+                        if (mExecutor != executor) {
+                            return;
                         }
+                        mCallback.onStarted();
                     });
                     break;
                 case GpsStatus.GPS_EVENT_STOPPED:
-                    executor.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (mExecutor != executor) {
-                                return;
-                            }
-                            mCallback.onStopped();
+                    executor.execute(() -> {
+                        if (mExecutor != executor) {
+                            return;
                         }
+                        mCallback.onStopped();
                     });
                     break;
                 case GpsStatus.GPS_EVENT_FIRST_FIX:
                     gpsStatus = mLocationManager.getGpsStatus(null);
                     if (gpsStatus != null) {
                         final int ttff = gpsStatus.getTimeToFirstFix();
-                        executor.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mExecutor != executor) {
-                                    return;
-                                }
-                                mCallback.onFirstFix(ttff);
+                        executor.execute(() -> {
+                            if (mExecutor != executor) {
+                                return;
                             }
+                            mCallback.onFirstFix(ttff);
                         });
                     }
                     break;
@@ -609,14 +910,11 @@
                     gpsStatus = mLocationManager.getGpsStatus(null);
                     if (gpsStatus != null) {
                         final GnssStatusCompat gnssStatus = GnssStatusCompat.wrap(gpsStatus);
-                        executor.execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mExecutor != executor) {
-                                    return;
-                                }
-                                mCallback.onSatelliteStatusChanged(gnssStatus);
+                        executor.execute(() -> {
+                            if (mExecutor != executor) {
+                                return;
                             }
+                            mCallback.onSatelliteStatusChanged(gnssStatus);
                         });
                     }
                     break;
@@ -624,6 +922,24 @@
         }
     }
 
+    @RequiresApi(31)
+    private static class Api31Impl {
+        private Api31Impl() {}
+
+        @DoNotInline
+        static boolean hasProvider(LocationManager locationManager, @NonNull String provider) {
+            return locationManager.hasProvider(provider);
+        }
+
+        @DoNotInline
+        @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
+        static void requestLocationUpdates(LocationManager locationManager,
+                @NonNull String provider, @NonNull LocationRequest locationRequest,
+                @NonNull Executor executor, @NonNull LocationListener listener) {
+            locationManager.requestLocationUpdates(provider, locationRequest, executor, listener);
+        }
+    }
+
     @RequiresApi(30)
     private static class Api30Impl {
         private Api30Impl() {}
@@ -639,12 +955,7 @@
                                 cancellationSignal.getCancellationSignalObject()
                             : null,
                     executor,
-                    new java.util.function.Consumer<Location>() {
-                        @Override
-                        public void accept(Location location) {
-                            consumer.accept(location);
-                        }
-                    });
+                    consumer::accept);
         }
     }
 
@@ -747,12 +1058,7 @@
             }
 
             final Consumer<Location> consumer = mConsumer;
-            mExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    consumer.accept(location);
-                }
-            });
+            mExecutor.execute(() -> consumer.accept(location));
 
             cleanup();
         }
diff --git a/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java b/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
new file mode 100644
index 0000000..d84ae0d
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/location/LocationRequestCompat.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright 2021 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 androidx.core.location;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.lang.Math.min;
+
+import android.location.LocationRequest;
+import android.os.Build.VERSION;
+
+import androidx.annotation.FloatRange;
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.core.util.TimeUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Compatibility version of {@link LocationRequest}.
+ */
+public final class LocationRequestCompat {
+
+    /**
+     * Represents a passive only request. Such a request will not trigger any active locations or
+     * power usage itself, but may receive locations generated in response to other requests.
+     *
+     * @see LocationRequestCompat#getIntervalMillis()
+     */
+    public static final long PASSIVE_INTERVAL = LocationRequest.PASSIVE_INTERVAL;
+
+    /** @hide */
+    @RestrictTo(LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({QUALITY_LOW_POWER, QUALITY_BALANCED_POWER_ACCURACY, QUALITY_HIGH_ACCURACY})
+    public @interface Quality {
+    }
+
+    /**
+     * A quality constant indicating a location provider may choose to satisfy this request by
+     * providing very accurate locations at the expense of potentially increased power usage. Each
+     * location provider may interpret this field differently, but as an example, the network
+     * provider may choose to return only wifi based locations rather than cell based locations in
+     * order to have greater accuracy when this flag is present.
+     */
+    public static final int QUALITY_HIGH_ACCURACY = LocationRequest.QUALITY_HIGH_ACCURACY;
+
+    /**
+     * A quality constant indicating a location provider may choose to satisfy this request by
+     * equally balancing power and accuracy constraints. Each location provider may interpret this
+     * field differently, but location providers will generally use their default behavior when this
+     * flag is present.
+     */
+    public static final int QUALITY_BALANCED_POWER_ACCURACY =
+            LocationRequest.QUALITY_BALANCED_POWER_ACCURACY;
+
+    /**
+     * A quality constant indicating a location provider may choose to satisfy this request by
+     * providing less accurate locations in order to save power. Each location provider may
+     * interpret this field differently, but as an example, the network provider may choose to
+     * return cell based locations rather than wifi based locations in order to save power when this
+     * flag is present.
+     */
+    public static final int QUALITY_LOW_POWER = LocationRequest.QUALITY_LOW_POWER;
+
+    private static final long IMPLICIT_MIN_UPDATE_INTERVAL = -1;
+
+    private static Method sCreateFromDeprecatedProviderMethod;
+    private static Method sSetQualityMethod;
+    private static Method sSetFastestIntervalMethod;
+    private static Method sSetNumUpdatesMethod;
+    private static Method sSetExpireInMethod;
+
+    @Quality
+    final int mQuality;
+    final long mIntervalMillis;
+    final long mMinUpdateIntervalMillis;
+    final long mDurationMillis;
+    final int mMaxUpdates;
+    final float mMinUpdateDistanceMeters;
+    final long mMaxUpdateDelayMillis;
+
+    LocationRequestCompat(
+            long intervalMillis,
+            @Quality int quality,
+            long durationMillis,
+            int maxUpdates,
+            long minUpdateIntervalMillis,
+            float minUpdateDistanceMeters,
+            long maxUpdateDelayMillis) {
+        mIntervalMillis = intervalMillis;
+        mQuality = quality;
+        mMinUpdateIntervalMillis = minUpdateIntervalMillis;
+        mDurationMillis = durationMillis;
+        mMaxUpdates = maxUpdates;
+        mMinUpdateDistanceMeters = minUpdateDistanceMeters;
+        mMaxUpdateDelayMillis = maxUpdateDelayMillis;
+    }
+
+    /**
+     * Returns the quality hint for this location request. The quality hint informs the provider how
+     * it should attempt to manage any accuracy vs power tradeoffs while attempting to satisfy this
+     * location request.
+     */
+    public @Quality int getQuality() {
+        return mQuality;
+    }
+
+    /**
+     * Returns the desired interval of location updates, or {@link #PASSIVE_INTERVAL} if this is a
+     * passive, no power request. A passive request will not actively generate location updates
+     * (and thus will not be power blamed for location), but may receive location updates generated
+     * as a result of other location requests. A passive request must always have an explicit
+     * minimum update interval set.
+     *
+     * <p>Locations may be available at a faster interval than specified here, see
+     * {@link #getMinUpdateIntervalMillis()} for the behavior in that case.
+     */
+    public @IntRange(from = 0) long getIntervalMillis() {
+        return mIntervalMillis;
+    }
+
+    /**
+     * Returns the minimum update interval. If location updates are available faster than the
+     * request interval then locations will only be updated if the minimum update interval has
+     * expired since the last location update.
+     *
+     * <p class=note><strong>Note:</strong> Some allowance for jitter is already built into the
+     * minimum update interval, so you need not worry about updates blocked simply because they
+     * arrived a fraction of a second earlier than expected.
+     *
+     * @return the minimum update interval
+     */
+    public @IntRange(from = 0) long getMinUpdateIntervalMillis() {
+        if (mMinUpdateIntervalMillis == IMPLICIT_MIN_UPDATE_INTERVAL) {
+            return mIntervalMillis;
+        } else {
+            return mMinUpdateIntervalMillis;
+        }
+    }
+
+    /**
+     * Returns the duration for which location will be provided before the request is automatically
+     * removed. A duration of <code>Long.MAX_VALUE</code> represents an unlimited duration.
+     *
+     * @return the duration for which location will be provided
+     */
+    public @IntRange(from = 1) long getDurationMillis() {
+        return mDurationMillis;
+    }
+
+    /**
+     * Returns the maximum number of location updates for this request before the request is
+     * automatically removed. A max updates value of <code>Integer.MAX_VALUE</code> represents an
+     * unlimited number of updates.
+     */
+    public @IntRange(from = 1, to = Integer.MAX_VALUE) int getMaxUpdates() {
+        return mMaxUpdates;
+    }
+
+    /**
+     * Returns the minimum distance between location updates. If a potential location update is
+     * closer to the last location update than the minimum update distance, then the potential
+     * location update will not occur. A value of 0 meters implies that no location update will ever
+     * be rejected due to failing this constraint.
+     *
+     * @return the minimum distance between location updates
+     */
+    public @FloatRange(from = 0, to = Float.MAX_VALUE) float getMinUpdateDistanceMeters() {
+        return mMinUpdateDistanceMeters;
+    }
+
+    /**
+     * Returns the maximum time any location update may be delayed, and thus grouped with following
+     * updates to enable location batching. If the maximum update delay is equal to or greater than
+     * twice the interval, then location providers may provide batched results. The maximum batch
+     * size is the maximum update delay divided by the interval. Not all devices or location
+     * providers support batching, and use of this parameter does not guarantee that the client will
+     * see batched results, or that batched results will always be of the maximum size.
+     *
+     * When available, batching can provide substantial power savings to the device, and clients are
+     * encouraged to take advantage where appropriate for the use case.
+     *
+     * @return the maximum time by which a location update may be delayed
+     * @see LocationListenerCompat#onLocationChanged(java.util.List)
+     */
+    public @IntRange(from = 0) long getMaxUpdateDelayMillis() {
+        return mMaxUpdateDelayMillis;
+    }
+
+    @RequiresApi(31)
+    @NonNull
+    LocationRequest toLocationRequest() {
+        return new LocationRequest.Builder(mIntervalMillis)
+                .setQuality(mQuality)
+                .setMinUpdateIntervalMillis(mMinUpdateIntervalMillis)
+                .setDurationMillis(mDurationMillis)
+                .setMaxUpdates(mMaxUpdates)
+                .setMinUpdateDistanceMeters(mMinUpdateDistanceMeters)
+                .setMaxUpdateDelayMillis(mMaxUpdateDelayMillis)
+                .build();
+    }
+
+    @SuppressWarnings("JavaReflectionMemberAccess")
+    @RequiresApi(19)
+    @NonNull
+    LocationRequest toLocationRequest(@NonNull String provider)
+            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+        if (VERSION.SDK_INT >= 31) {
+            return toLocationRequest();
+        } else if (VERSION.SDK_INT >= 19) {
+            if (sCreateFromDeprecatedProviderMethod == null) {
+                sCreateFromDeprecatedProviderMethod = LocationRequest.class.getDeclaredMethod(
+                        "createFromDeprecatedProvider", String.class, long.class, float.class,
+                        boolean.class);
+                sCreateFromDeprecatedProviderMethod.setAccessible(true);
+            }
+
+            LocationRequest request =
+                    (LocationRequest) sCreateFromDeprecatedProviderMethod.invoke(null, provider,
+                            mIntervalMillis,
+                            mMinUpdateDistanceMeters, false);
+            if (request == null) {
+                // should never happen
+                throw new InvocationTargetException(new NullPointerException());
+            }
+
+            if (sSetQualityMethod == null) {
+                sSetQualityMethod = LocationRequest.class.getDeclaredMethod(
+                        "setQuality", int.class);
+                sSetQualityMethod.setAccessible(true);
+            }
+            sSetQualityMethod.invoke(request, mQuality);
+
+            if (getMinUpdateIntervalMillis() != mIntervalMillis) {
+                if (sSetFastestIntervalMethod == null) {
+                    sSetFastestIntervalMethod = LocationRequest.class.getDeclaredMethod(
+                            "setFastestInterval", long.class);
+                    sSetFastestIntervalMethod.setAccessible(true);
+                }
+
+                sSetFastestIntervalMethod.invoke(request, mMinUpdateIntervalMillis);
+            }
+
+            if (mMaxUpdates < Integer.MAX_VALUE) {
+                if (sSetNumUpdatesMethod == null) {
+                    sSetNumUpdatesMethod = LocationRequest.class.getDeclaredMethod(
+                            "setNumUpdates", int.class);
+                    sSetNumUpdatesMethod.setAccessible(true);
+                }
+
+                sSetNumUpdatesMethod.invoke(request, mMaxUpdates);
+            }
+
+            if (mDurationMillis < Long.MAX_VALUE) {
+                if (sSetExpireInMethod == null) {
+                    sSetExpireInMethod = LocationRequest.class.getDeclaredMethod(
+                            "setExpireIn", long.class);
+                    sSetExpireInMethod.setAccessible(true);
+                }
+
+                sSetExpireInMethod.invoke(request, mDurationMillis);
+            }
+            return request;
+        }
+
+        throw new NoClassDefFoundError();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof LocationRequestCompat)) {
+            return false;
+        }
+
+        LocationRequestCompat that = (LocationRequestCompat) o;
+        return mQuality == that.mQuality && mIntervalMillis == that.mIntervalMillis
+                && mMinUpdateIntervalMillis == that.mMinUpdateIntervalMillis
+                && mDurationMillis == that.mDurationMillis && mMaxUpdates == that.mMaxUpdates
+                && Float.compare(that.mMinUpdateDistanceMeters, mMinUpdateDistanceMeters) == 0
+                && mMaxUpdateDelayMillis == that.mMaxUpdateDelayMillis;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mQuality;
+        result = 31 * result + (int) (mIntervalMillis ^ (mIntervalMillis >>> 32));
+        result = 31 * result + (int) (mMinUpdateIntervalMillis ^ (mMinUpdateIntervalMillis >>> 32));
+        return result;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        StringBuilder s = new StringBuilder();
+        s.append("Request[");
+        if (mIntervalMillis != PASSIVE_INTERVAL) {
+            s.append("@");
+            TimeUtils.formatDuration(mIntervalMillis, s);
+
+            switch (mQuality) {
+                case QUALITY_HIGH_ACCURACY:
+                    s.append(" HIGH_ACCURACY");
+                    break;
+                case QUALITY_BALANCED_POWER_ACCURACY:
+                    s.append(" BALANCED");
+                    break;
+                case QUALITY_LOW_POWER:
+                    s.append(" LOW_POWER");
+                    break;
+            }
+        } else {
+            s.append("PASSIVE");
+        }
+        if (mDurationMillis != Long.MAX_VALUE) {
+            s.append(", duration=");
+            TimeUtils.formatDuration(mDurationMillis, s);
+        }
+        if (mMaxUpdates != Integer.MAX_VALUE) {
+            s.append(", maxUpdates=").append(mMaxUpdates);
+        }
+        if (mMinUpdateIntervalMillis != IMPLICIT_MIN_UPDATE_INTERVAL
+                && mMinUpdateIntervalMillis < mIntervalMillis) {
+            s.append(", minUpdateInterval=");
+            TimeUtils.formatDuration(mMinUpdateIntervalMillis, s);
+        }
+        if (mMinUpdateDistanceMeters > 0.0) {
+            s.append(", minUpdateDistance=").append(mMinUpdateDistanceMeters);
+        }
+        if (mMaxUpdateDelayMillis / 2 > mIntervalMillis) {
+            s.append(", maxUpdateDelay=");
+            TimeUtils.formatDuration(mMaxUpdateDelayMillis, s);
+        }
+        s.append(']');
+        return s.toString();
+    }
+
+    /**
+     * A builder class for {@link LocationRequestCompat}.
+     */
+    public static final class Builder {
+
+        private long mIntervalMillis;
+        private @Quality int mQuality;
+        private long mDurationMillis;
+        private int mMaxUpdates;
+        private long mMinUpdateIntervalMillis;
+        private float mMinUpdateDistanceMeters;
+        private long mMaxUpdateDelayMillis;
+
+        /**
+         * Creates a new Builder with the given interval. See {@link #setIntervalMillis(long)} for
+         * more information on the interval. Note that the defaults for various Builder parameters
+         * may be different from the defaults for the framework {@link LocationRequest}.
+         */
+        public Builder(long intervalMillis) {
+            // gives us a range check
+            setIntervalMillis(intervalMillis);
+
+            mQuality = QUALITY_BALANCED_POWER_ACCURACY;
+            mDurationMillis = Long.MAX_VALUE;
+            mMaxUpdates = Integer.MAX_VALUE;
+            mMinUpdateIntervalMillis = IMPLICIT_MIN_UPDATE_INTERVAL;
+            mMinUpdateDistanceMeters = 0;
+            mMaxUpdateDelayMillis = 0;
+        }
+
+        /**
+         * Creates a new Builder with all parameters copied from the given location request.
+         */
+        public Builder(@NonNull LocationRequestCompat locationRequest) {
+            mIntervalMillis = locationRequest.mIntervalMillis;
+            mQuality = locationRequest.mQuality;
+            mDurationMillis = locationRequest.mDurationMillis;
+            mMaxUpdates = locationRequest.mMaxUpdates;
+            mMinUpdateIntervalMillis = locationRequest.mMinUpdateIntervalMillis;
+            mMinUpdateDistanceMeters = locationRequest.mMinUpdateDistanceMeters;
+            mMaxUpdateDelayMillis = locationRequest.mMaxUpdateDelayMillis;
+        }
+
+        /**
+         * Sets the request interval. The request interval may be set to {@link #PASSIVE_INTERVAL}
+         * which indicates this request will not actively generate location updates (and thus will
+         * not be power blamed for location), but may receive location updates generated as a result
+         * of other location requests. A passive request must always have an explicit minimum
+         * update interval set.
+         *
+         * <p>Locations may be available at a faster interval than specified here, see
+         * {@link #setMinUpdateIntervalMillis(long)} for the behavior in that case.
+         *
+         * <p class="note"><strong>Note:</strong> On platforms below Android 12, using the
+         * {@link #PASSIVE_INTERVAL} will not result in a truly passive request, but a request with
+         * an extremely long interval. In most cases, this is effectively the same as a passive
+         * request, but this may occasionally result in an initial location calculation for which
+         * the client will be blamed.
+         */
+        public @NonNull Builder setIntervalMillis(@IntRange(from = 0) long intervalMillis) {
+            mIntervalMillis = Preconditions.checkArgumentInRange(intervalMillis, 0, Long
+                            .MAX_VALUE,
+                    "intervalMillis");
+            return this;
+        }
+
+        /**
+         * Sets the request quality. The quality is a hint to providers on how they should weigh
+         * power vs accuracy tradeoffs. High accuracy locations may cost more power to produce, and
+         * lower accuracy locations may cost less power to produce. Defaults to
+         * {@link #QUALITY_BALANCED_POWER_ACCURACY}.
+         */
+        public @NonNull Builder setQuality(@Quality int quality) {
+            Preconditions.checkArgument(
+                    quality == QUALITY_LOW_POWER || quality == QUALITY_BALANCED_POWER_ACCURACY
+                            || quality == QUALITY_HIGH_ACCURACY,
+                    "quality must be a defined QUALITY constant, not %d", quality);
+            mQuality = quality;
+            return this;
+        }
+
+        /**
+         * Sets the duration this request will continue before being automatically removed. Defaults
+         * to <code>Long.MAX_VALUE</code>, which represents an unlimited duration.
+         *
+         * <p class="note"><strong>Note:</strong> This parameter will be ignored on platforms below
+         * Android Kitkat, and the request will not be removed after the duration expires.
+         */
+        public @NonNull Builder setDurationMillis(@IntRange(from = 1) long durationMillis) {
+            mDurationMillis = Preconditions.checkArgumentInRange(durationMillis, 1, Long
+                            .MAX_VALUE,
+                    "durationMillis");
+            return this;
+        }
+
+        /**
+         * Sets the maximum number of location updates for this request before this request is
+         * automatically removed. Defaults to <code>Integer.MAX_VALUE</code>, which represents an
+         * unlimited number of updates.
+         */
+        public @NonNull Builder setMaxUpdates(
+                @IntRange(from = 1, to = Integer.MAX_VALUE) int maxUpdates) {
+            mMaxUpdates = Preconditions.checkArgumentInRange(maxUpdates, 1, Integer.MAX_VALUE,
+                    "maxUpdates");
+            return this;
+        }
+
+        /**
+         * Sets an explicit minimum update interval. If location updates are available faster than
+         * the request interval then an update will only occur if the minimum update interval has
+         * expired since the last location update. Defaults to no explicit minimum update interval
+         * set, which means the minimum update interval is the same as the interval.
+         *
+         * <p class=note><strong>Note:</strong> Some allowance for jitter is already built into the
+         * minimum update interval, so you need not worry about updates blocked simply because they
+         * arrived a fraction of a second earlier than expected.
+         *
+         * <p class="note"><strong>Note:</strong> When {@link #build()} is invoked, the minimum of
+         * the interval and the minimum update interval will be used as the minimum update interval
+         * of the built request.
+         */
+        public @NonNull Builder setMinUpdateIntervalMillis(
+                @IntRange(from = 0) long minUpdateIntervalMillis) {
+            mMinUpdateIntervalMillis = Preconditions.checkArgumentInRange(minUpdateIntervalMillis,
+                    0, Long.MAX_VALUE, "minUpdateIntervalMillis");
+            return this;
+        }
+
+        /**
+         * Clears an explicitly set minimum update interval and reverts to an implicit minimum
+         * update interval (ie, the minimum update interval is the same value as the interval).
+         */
+        public @NonNull Builder clearMinUpdateIntervalMillis() {
+            mMinUpdateIntervalMillis = IMPLICIT_MIN_UPDATE_INTERVAL;
+            return this;
+        }
+
+        /**
+         * Sets the minimum update distance between location updates. If a potential location
+         * update is closer to the last location update than the minimum update distance, then
+         * the potential location update will not occur. Defaults to 0, which represents no minimum
+         * update distance.
+         */
+        public @NonNull Builder setMinUpdateDistanceMeters(
+                @FloatRange(from = 0, to = Float.MAX_VALUE) float minUpdateDistanceMeters) {
+            mMinUpdateDistanceMeters = minUpdateDistanceMeters;
+            mMinUpdateDistanceMeters = Preconditions.checkArgumentInRange(minUpdateDistanceMeters,
+                    0, Float.MAX_VALUE, "minUpdateDistanceMeters");
+            return this;
+        }
+
+        /**
+         * Sets the maximum time any location update may be delayed, and thus grouped with following
+         * updates to enable location batching. If the maximum update delay is equal to or greater
+         * than twice the interval, then location providers may provide batched results. Defaults to
+         * 0, which represents no batching allowed.
+         */
+        public @NonNull Builder setMaxUpdateDelayMillis(
+                @IntRange(from = 0) long maxUpdateDelayMillis) {
+            mMaxUpdateDelayMillis = maxUpdateDelayMillis;
+            mMaxUpdateDelayMillis = Preconditions.checkArgumentInRange(maxUpdateDelayMillis, 0,
+                    Long.MAX_VALUE, "maxUpdateDelayMillis");
+            return this;
+        }
+
+        /**
+         * Builds a location request from this builder. If an explicit minimum update interval is
+         * set, the minimum update interval of the location request will be the minimum of the
+         * interval and minimum update interval.
+         *
+         * <p>If building a passive request then you must have set an explicit minimum update
+         * interval.
+         */
+        public @NonNull LocationRequestCompat build() {
+            Preconditions.checkState(mIntervalMillis != PASSIVE_INTERVAL
+                            || mMinUpdateIntervalMillis != IMPLICIT_MIN_UPDATE_INTERVAL,
+                    "passive location requests must have an explicit minimum update interval");
+
+            return new LocationRequestCompat(
+                    mIntervalMillis,
+                    mQuality,
+                    mDurationMillis,
+                    mMaxUpdates,
+                    min(mMinUpdateIntervalMillis, mIntervalMillis),
+                    mMinUpdateDistanceMeters,
+                    mMaxUpdateDelayMillis);
+        }
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/util/Preconditions.java b/core/core/src/main/java/androidx/core/util/Preconditions.java
index 9a759f8..ce979e0 100644
--- a/core/core/src/main/java/androidx/core/util/Preconditions.java
+++ b/core/core/src/main/java/androidx/core/util/Preconditions.java
@@ -54,6 +54,24 @@
     }
 
     /**
+     * Ensures that an expression checking an argument is true.
+     *
+     * @param expression the expression to check
+     * @param messageTemplate a printf-style message template to use if the check fails; will
+     *     be converted to a string using {@link String#format(String, Object...)}
+     * @param messageArgs arguments for {@code messageTemplate}
+     * @throws IllegalArgumentException if {@code expression} is false
+     */
+    public static void checkArgument(
+            final boolean expression,
+            final @NonNull String messageTemplate,
+            final @NonNull Object... messageArgs) {
+        if (!expression) {
+            throw new IllegalArgumentException(String.format(messageTemplate, messageArgs));
+        }
+    }
+
+    /**
      * Ensures that an string reference passed as a parameter to the calling
      * method is not empty.
      *
@@ -237,6 +255,87 @@
         return value;
     }
 
+    /**
+     * Ensures that the argument long value is within the inclusive range.
+     *
+     * @param value a long value
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated long value
+     *
+     * @throws IllegalArgumentException if {@code value} was not within the range
+     */
+    public static long checkArgumentInRange(long value, long lower, long upper,
+            @NonNull String valueName) {
+        if (value < lower) {
+            throw new IllegalArgumentException(
+                    String.format(Locale.US,
+                            "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
+        } else if (value > upper) {
+            throw new IllegalArgumentException(
+                    String.format(Locale.US,
+                            "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the argument float value is within the inclusive range.
+     *
+     * @param value a float value
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated float value
+     *
+     * @throws IllegalArgumentException if {@code value} was not within the range
+     */
+    public static float checkArgumentInRange(float value, float lower, float upper,
+            @NonNull String valueName) {
+        if (value < lower) {
+            throw new IllegalArgumentException(
+                    String.format(Locale.US,
+                            "%s is out of range of [%f, %f] (too low)", valueName, lower, upper));
+        } else if (value > upper) {
+            throw new IllegalArgumentException(
+                    String.format(Locale.US,
+                            "%s is out of range of [%f, %f] (too high)", valueName, lower, upper));
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the argument double value is within the inclusive range.
+     *
+     * @param value a double value
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated double value
+     *
+     * @throws IllegalArgumentException if {@code value} was not within the range
+     */
+    public static double checkArgumentInRange(double value, double lower, double upper,
+            @NonNull String valueName) {
+        if (value < lower) {
+            throw new IllegalArgumentException(
+                    String.format(Locale.US,
+                            "%s is out of range of [%f, %f] (too low)", valueName, lower, upper));
+        } else if (value > upper) {
+            throw new IllegalArgumentException(
+                    String.format(Locale.US,
+                            "%s is out of range of [%f, %f] (too high)", valueName, lower, upper));
+        }
+
+        return value;
+    }
+
     private Preconditions() {
     }
 }
diff --git a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
index ec5f385..e29026b 100644
--- a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
@@ -19,12 +19,16 @@
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.util.Pair;
+import android.view.ContentInfo;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.core.util.Preconditions;
 
@@ -32,12 +36,12 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Predicate;
 
 /**
- * Holds all the relevant data for a request to {@link OnReceiveContentListener}.
+ * Holds all the relevant data for a request to {@link OnReceiveContentListener}. This is a
+ * backward-compatible wrapper for the platform class {@link ContentInfo}.
  */
-// This class has the "Compat" suffix because it will integrate with (ie, wrap) the SDK API once
-// that is available.
 public final class ContentInfoCompat {
 
     /**
@@ -47,7 +51,8 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    @IntDef(value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD, SOURCE_DRAG_AND_DROP})
+    @IntDef(value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD, SOURCE_DRAG_AND_DROP,
+            SOURCE_AUTOFILL, SOURCE_PROCESS_TEXT})
     @Retention(RetentionPolicy.SOURCE)
     public @interface Source {
     }
@@ -77,6 +82,19 @@
     public static final int SOURCE_DRAG_AND_DROP = 3;
 
     /**
+     * Specifies that the operation was triggered by the autofill framework. See
+     * https://developer.android.com/guide/topics/text/autofill for more info.
+     */
+    public static final int SOURCE_AUTOFILL = 4;
+
+    /**
+     * Specifies that the operation was triggered by a result from a
+     * {@link android.content.Intent#ACTION_PROCESS_TEXT PROCESS_TEXT} action in the selection
+     * menu.
+     */
+    public static final int SOURCE_PROCESS_TEXT = 5;
+
+    /**
      * Returns the symbolic name of the given source.
      *
      * @hide
@@ -89,6 +107,8 @@
             case SOURCE_CLIPBOARD: return "SOURCE_CLIPBOARD";
             case SOURCE_INPUT_METHOD: return "SOURCE_INPUT_METHOD";
             case SOURCE_DRAG_AND_DROP: return "SOURCE_DRAG_AND_DROP";
+            case SOURCE_AUTOFILL: return "SOURCE_AUTOFILL";
+            case SOURCE_PROCESS_TEXT: return "SOURCE_PROCESS_TEXT";
         }
         return String.valueOf(source);
     }
@@ -124,35 +144,46 @@
     }
 
     @NonNull
-    final ClipData mClip;
-    @Source
-    final int mSource;
-    @Flags
-    final int mFlags;
-    @Nullable
-    final Uri mLinkUri;
-    @Nullable
-    final Bundle mExtras;
+    private final Compat mCompat;
 
-    ContentInfoCompat(Builder b) {
-        this.mClip = Preconditions.checkNotNull(b.mClip);
-        this.mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_DRAG_AND_DROP,
-                "source");
-        this.mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT);
-        this.mLinkUri = b.mLinkUri;
-        this.mExtras = b.mExtras;
+    ContentInfoCompat(@NonNull Compat compat) {
+        mCompat = compat;
+    }
+
+    /**
+     * Provides a backward-compatible wrapper for {@link ContentInfo}.
+     *
+     * <p>This method is not supported on devices running SDK <= 30 since the platform
+     * class will not be available.
+     *
+     * @param platContentInfo platform class to wrap, must not be null
+     * @return wrapped class
+     */
+    @RequiresApi(31)
+    @NonNull
+    public static ContentInfoCompat toContentInfoCompat(@NonNull ContentInfo platContentInfo) {
+        return new ContentInfoCompat(new Compat31Impl(platContentInfo));
+    }
+
+    /**
+     * Provides the {@link ContentInfo} represented by this object.
+     *
+     * <p>This method is not supported on devices running SDK <= 30 since the platform
+     * class will not be available.
+     *
+     * @return platform class object
+     * @see ContentInfoCompat#toContentInfoCompat
+     */
+    @RequiresApi(31)
+    @NonNull
+    public ContentInfo toContentInfo() {
+        return mCompat.getWrapped();
     }
 
     @NonNull
     @Override
     public String toString() {
-        return "ContentInfoCompat{"
-                + "clip=" + mClip.getDescription()
-                + ", source=" + sourceToString(mSource)
-                + ", flags=" + flagsToString(mFlags)
-                + (mLinkUri == null ? "" : ", hasLinkUri(" + mLinkUri.toString().length() + ")")
-                + (mExtras == null ? "" : ", hasExtras")
-                + "}";
+        return mCompat.toString();
     }
 
     /**
@@ -160,7 +191,7 @@
      */
     @NonNull
     public ClipData getClip() {
-        return mClip;
+        return mCompat.getClip();
     }
 
     /**
@@ -169,7 +200,7 @@
      */
     @Source
     public int getSource() {
-        return mSource;
+        return mCompat.getSource();
     }
 
     /**
@@ -177,7 +208,7 @@
      */
     @Flags
     public int getFlags() {
-        return mFlags;
+        return mCompat.getFlags();
     }
 
     /**
@@ -188,7 +219,7 @@
      */
     @Nullable
     public Uri getLinkUri() {
-        return mLinkUri;
+        return mCompat.getLinkUri();
     }
 
     /**
@@ -198,7 +229,7 @@
      */
     @Nullable
     public Bundle getExtras() {
-        return mExtras;
+        return mCompat.getExtras();
     }
 
     /**
@@ -220,37 +251,51 @@
     @NonNull
     public Pair<ContentInfoCompat, ContentInfoCompat> partition(
             @NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
-        if (mClip.getItemCount() == 1) {
-            boolean matched = itemPredicate.test(mClip.getItemAt(0));
+        ClipData clip = mCompat.getClip();
+        if (clip.getItemCount() == 1) {
+            boolean matched = itemPredicate.test(clip.getItemAt(0));
             return Pair.create(matched ? this : null, matched ? null : this);
         }
-        ArrayList<ClipData.Item> acceptedItems = new ArrayList<>();
-        ArrayList<ClipData.Item> remainingItems = new ArrayList<>();
-        for (int i = 0; i < mClip.getItemCount(); i++) {
-            ClipData.Item item = mClip.getItemAt(i);
+        Pair<ClipData, ClipData> split = ContentInfoCompat.partition(clip, itemPredicate);
+        if (split.first == null) {
+            return Pair.create(null, this);
+        } else if (split.second == null) {
+            return Pair.create(this, null);
+        }
+        return Pair.create(
+                new ContentInfoCompat.Builder(this).setClip(split.first).build(),
+                new ContentInfoCompat.Builder(this).setClip(split.second).build());
+    }
+
+    @NonNull
+    static Pair<ClipData, ClipData> partition(@NonNull ClipData clip,
+            @NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
+        ArrayList<ClipData.Item> acceptedItems = null;
+        ArrayList<ClipData.Item> remainingItems = null;
+        for (int i = 0; i < clip.getItemCount(); i++) {
+            ClipData.Item item = clip.getItemAt(i);
             if (itemPredicate.test(item)) {
+                acceptedItems = (acceptedItems == null) ? new ArrayList<>() : acceptedItems;
                 acceptedItems.add(item);
             } else {
+                remainingItems = (remainingItems == null) ? new ArrayList<>() : remainingItems;
                 remainingItems.add(item);
             }
         }
-        if (acceptedItems.isEmpty()) {
-            return Pair.create(null, this);
+        if (acceptedItems == null) {
+            return Pair.create(null, clip);
         }
-        if (remainingItems.isEmpty()) {
-            return Pair.create(this, null);
+        if (remainingItems == null) {
+            return Pair.create(clip, null);
         }
-        ContentInfoCompat accepted = new Builder(this)
-                .setClip(buildClipData(mClip.getDescription(), acceptedItems))
-                .build();
-        ContentInfoCompat remaining = new Builder(this)
-                .setClip(buildClipData(mClip.getDescription(), remainingItems))
-                .build();
-        return Pair.create(accepted, remaining);
+        return Pair.create(
+                buildClipData(clip.getDescription(), acceptedItems),
+                buildClipData(clip.getDescription(), remainingItems));
     }
 
-    private static ClipData buildClipData(ClipDescription description,
-            List<ClipData.Item> items) {
+    @NonNull
+    static ClipData buildClipData(@NonNull ClipDescription description,
+            @NonNull List<ClipData.Item> items) {
         ClipData clip = new ClipData(new ClipDescription(description), items.get(0));
         for (int i = 1; i < items.size(); i++) {
             clip.addItem(items.get(i));
@@ -259,29 +304,205 @@
     }
 
     /**
+     * Partitions content based on the given predicate.
+     *
+     * <p>This function classifies the content and organizes it into a pair, grouping the items
+     * that matched vs didn't match the predicate.
+     *
+     * <p>Except for the {@link ClipData} items, the returned objects will contain all the same
+     * metadata as the passed-in {@link ContentInfo}.
+     *
+     * @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which
+     *                      partition to place it into.
+     * @return A pair containing the partitioned content. The pair's first object will have the
+     * content that matched the predicate, or null if none of the items matched. The pair's
+     * second object will have the content that didn't match the predicate, or null if all of
+     * the items matched.
+     */
+    @RequiresApi(31)
+    @NonNull
+    public static Pair<ContentInfo, ContentInfo> partition(@NonNull ContentInfo payload,
+            @NonNull Predicate<ClipData.Item> itemPredicate) {
+        return Api31Impl.partition(payload, itemPredicate);
+    }
+
+    @RequiresApi(31)
+    private static final class Api31Impl {
+        private Api31Impl() {}
+
+        @DoNotInline
+        @NonNull
+        public static Pair<ContentInfo, ContentInfo> partition(@NonNull ContentInfo payload,
+                @NonNull Predicate<ClipData.Item> itemPredicate) {
+            ClipData clip = payload.getClip();
+            if (clip.getItemCount() == 1) {
+                boolean matched = itemPredicate.test(clip.getItemAt(0));
+                return Pair.create(matched ? payload : null, matched ? null : payload);
+            }
+            Pair<ClipData, ClipData> split = ContentInfoCompat.partition(clip, itemPredicate::test);
+            if (split.first == null) {
+                return Pair.create(null, payload);
+            } else if (split.second == null) {
+                return Pair.create(payload, null);
+            }
+            return Pair.create(
+                    new ContentInfo.Builder(payload).setClip(split.first).build(),
+                    new ContentInfo.Builder(payload).setClip(split.second).build());
+        }
+    }
+
+    private interface Compat {
+        @Nullable
+        ContentInfo getWrapped();
+        @NonNull
+        ClipData getClip();
+        @Source
+        int getSource();
+        @Flags
+        int getFlags();
+        @Nullable
+        Uri getLinkUri();
+        @Nullable
+        Bundle getExtras();
+    }
+
+    private static final class CompatImpl implements Compat {
+        @NonNull
+        private final ClipData mClip;
+        @Source
+        private final int mSource;
+        @Flags
+        private final int mFlags;
+        @Nullable
+        private final Uri mLinkUri;
+        @Nullable
+        private final Bundle mExtras;
+
+        CompatImpl(BuilderCompatImpl b) {
+            mClip = Preconditions.checkNotNull(b.mClip);
+            mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_PROCESS_TEXT,
+                    "source");
+            mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT);
+            mLinkUri = b.mLinkUri;
+            mExtras = b.mExtras;
+        }
+
+        @Nullable
+        @Override
+        public ContentInfo getWrapped() {
+            return null;
+        }
+
+        @NonNull
+        @Override
+        public ClipData getClip() {
+            return mClip;
+        }
+
+        @Source
+        @Override
+        public int getSource() {
+            return mSource;
+        }
+
+        @Flags
+        @Override
+        public int getFlags() {
+            return mFlags;
+        }
+
+        @Nullable
+        @Override
+        public Uri getLinkUri() {
+            return mLinkUri;
+        }
+
+        @Nullable
+        @Override
+        public Bundle getExtras() {
+            return mExtras;
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return "ContentInfoCompat{"
+                    + "clip=" + mClip.getDescription()
+                    + ", source=" + sourceToString(mSource)
+                    + ", flags=" + flagsToString(mFlags)
+                    + (mLinkUri == null ? "" : ", hasLinkUri(" + mLinkUri.toString().length() + ")")
+                    + (mExtras == null ? "" : ", hasExtras")
+                    + "}";
+        }
+    }
+
+    private static final class Compat31Impl implements Compat {
+        @NonNull
+        private final ContentInfo mWrapped;
+
+        Compat31Impl(@NonNull ContentInfo wrapped) {
+            mWrapped = Preconditions.checkNotNull(wrapped);
+        }
+
+        @NonNull
+        @Override
+        public ContentInfo getWrapped() {
+            return mWrapped;
+        }
+
+        @NonNull
+        @Override
+        public ClipData getClip() {
+            return mWrapped.getClip();
+        }
+
+        @Source
+        @Override
+        public int getSource() {
+            return mWrapped.getSource();
+        }
+
+        @Flags
+        @Override
+        public int getFlags() {
+            return mWrapped.getFlags();
+        }
+
+        @Nullable
+        @Override
+        public Uri getLinkUri() {
+            return mWrapped.getLinkUri();
+        }
+
+        @Nullable
+        @Override
+        public Bundle getExtras() {
+            return mWrapped.getExtras();
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return "ContentInfoCompat{" + mWrapped + "}";
+        }
+    }
+
+    /**
      * Builder for {@link ContentInfoCompat}.
      */
     public static final class Builder {
         @NonNull
-        ClipData mClip;
-        @Source
-        int mSource;
-        @Flags
-        int mFlags;
-        @Nullable
-        Uri mLinkUri;
-        @Nullable
-        Bundle mExtras;
+        private final BuilderCompat mBuilderCompat;
 
         /**
-         * Creates a new builder initialized with the data from the given builder.
+         * Creates a new builder initialized with the data from the given object (shallow copy).
          */
         public Builder(@NonNull ContentInfoCompat other) {
-            mClip = other.mClip;
-            mSource = other.mSource;
-            mFlags = other.mFlags;
-            mLinkUri = other.mLinkUri;
-            mExtras = other.mExtras;
+            if (Build.VERSION.SDK_INT >= 31) {
+                mBuilderCompat = new BuilderCompat31Impl(other);
+            } else {
+                mBuilderCompat = new BuilderCompatImpl(other);
+            }
         }
 
         /**
@@ -291,8 +512,11 @@
          * @param source The source of the operation. See {@code SOURCE_} constants.
          */
         public Builder(@NonNull ClipData clip, @Source int source) {
-            mClip = clip;
-            mSource = source;
+            if (Build.VERSION.SDK_INT >= 31) {
+                mBuilderCompat = new BuilderCompat31Impl(clip, source);
+            } else {
+                mBuilderCompat = new BuilderCompatImpl(clip, source);
+            }
         }
 
         /**
@@ -303,7 +527,7 @@
          */
         @NonNull
         public Builder setClip(@NonNull ClipData clip) {
-            mClip = clip;
+            mBuilderCompat.setClip(clip);
             return this;
         }
 
@@ -315,7 +539,7 @@
          */
         @NonNull
         public Builder setSource(@Source int source) {
-            mSource = source;
+            mBuilderCompat.setSource(source);
             return this;
         }
 
@@ -328,7 +552,7 @@
          */
         @NonNull
         public Builder setFlags(@Flags int flags) {
-            mFlags = flags;
+            mBuilderCompat.setFlags(flags);
             return this;
         }
 
@@ -341,7 +565,7 @@
          */
         @NonNull
         public Builder setLinkUri(@Nullable Uri linkUri) {
-            mLinkUri = linkUri;
+            mBuilderCompat.setLinkUri(linkUri);
             return this;
         }
 
@@ -353,7 +577,7 @@
          */
         @NonNull
         public Builder setExtras(@Nullable Bundle extras) {
-            mExtras = extras;
+            mBuilderCompat.setExtras(extras);
             return this;
         }
 
@@ -362,7 +586,119 @@
          */
         @NonNull
         public ContentInfoCompat build() {
-            return new ContentInfoCompat(this);
+            return mBuilderCompat.build();
+        }
+    }
+
+    private interface BuilderCompat {
+        void setClip(@NonNull ClipData clip);
+        void setSource(@Source int source);
+        void setFlags(@Flags int flags);
+        void setLinkUri(@Nullable Uri linkUri);
+        void setExtras(@Nullable Bundle extras);
+        @NonNull
+        ContentInfoCompat build();
+    }
+
+    private static final class BuilderCompatImpl implements BuilderCompat {
+        @NonNull
+        ClipData mClip;
+        @Source
+        int mSource;
+        @Flags
+        int mFlags;
+        @Nullable
+        Uri mLinkUri;
+        @Nullable
+        Bundle mExtras;
+
+        BuilderCompatImpl(@NonNull ClipData clip, int source) {
+            mClip = clip;
+            mSource = source;
+        }
+
+        BuilderCompatImpl(@NonNull ContentInfoCompat other) {
+            mClip = other.getClip();
+            mSource = other.getSource();
+            mFlags = other.getFlags();
+            mLinkUri = other.getLinkUri();
+            mExtras = other.getExtras();
+        }
+
+        @Override
+        public void setClip(@NonNull ClipData clip) {
+            mClip = clip;
+        }
+
+        @Override
+        public void setSource(@Source int source) {
+            mSource = source;
+        }
+
+        @Override
+        public void setFlags(@Flags int flags) {
+            mFlags = flags;
+        }
+
+        @Override
+        public void setLinkUri(@Nullable Uri linkUri) {
+            mLinkUri = linkUri;
+        }
+
+        @Override
+        public void setExtras(@Nullable Bundle extras) {
+            mExtras = extras;
+        }
+
+        @Override
+        @NonNull
+        public ContentInfoCompat build() {
+            return new ContentInfoCompat(new CompatImpl(this));
+        }
+    }
+
+    @RequiresApi(31)
+    private static final class BuilderCompat31Impl implements BuilderCompat {
+        @NonNull
+        private final ContentInfo.Builder mPlatformBuilder;
+
+        BuilderCompat31Impl(@NonNull ClipData clip, int source) {
+            mPlatformBuilder = new ContentInfo.Builder(clip, source);
+        }
+
+        BuilderCompat31Impl(@NonNull ContentInfoCompat other) {
+            mPlatformBuilder = new ContentInfo.Builder(other.toContentInfo());
+        }
+
+        @Override
+        public void setClip(@NonNull ClipData clip) {
+            mPlatformBuilder.setClip(clip);
+        }
+
+        @Override
+        public void setSource(@Source int source) {
+            mPlatformBuilder.setSource(source);
+        }
+
+        @Override
+        public void setFlags(@Flags int flags) {
+            mPlatformBuilder.setFlags(flags);
+        }
+
+        @Override
+        public void setLinkUri(@Nullable Uri linkUri) {
+            mPlatformBuilder.setLinkUri(linkUri);
+        }
+
+        @Override
+        public void setExtras(@Nullable Bundle extras) {
+            mPlatformBuilder.setExtras(extras);
+        }
+
+        @NonNull
+        @Override
+        public ContentInfoCompat build() {
+            return new ContentInfoCompat(new Compat31Impl(mPlatformBuilder.build()));
         }
     }
 }
diff --git a/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
index 9b67b78..16156c8 100644
--- a/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
+++ b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
@@ -23,10 +23,13 @@
  * Interface for widgets to implement default behavior for receiving content. Content may be both
  * text and non-text (plain/styled text, HTML, images, videos, audio files, etc).
  *
- * <p>Widgets should implement this interface to define the default behavior for receiving content.
- * Apps wishing to provide custom behavior for receiving content should set a listener via
- * {@link ViewCompat#setOnReceiveContentListener}. See {@link ViewCompat#performReceiveContent} for
- * more info.
+ * <p>Widgets should implement this interface to define the default behavior for receiving content
+ * when the SDK is <= 30. When doing so, widgets should also override
+ * {@link android.view.View#onReceiveContent} for SDK > 30.
+ *
+ * <p>Apps wishing to provide custom behavior for receiving content should not implement this
+ * interface but rather set a listener via {@link ViewCompat#setOnReceiveContentListener}. See
+ * {@link ViewCompat#performReceiveContent} for more info.
  */
 public interface OnReceiveContentViewBehavior {
     /**
diff --git a/core/core/src/main/java/androidx/core/view/ViewCompat.java b/core/core/src/main/java/androidx/core/view/ViewCompat.java
index 4fb240c..120149c 100644
--- a/core/core/src/main/java/androidx/core/view/ViewCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewCompat.java
@@ -38,6 +38,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.SparseArray;
+import android.view.ContentInfo;
 import android.view.Display;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -2749,6 +2750,10 @@
      */
     public static void setOnReceiveContentListener(@NonNull View view, @Nullable String[] mimeTypes,
             @Nullable OnReceiveContentListener listener) {
+        if (Build.VERSION.SDK_INT >= 31) {
+            Api31Impl.setOnReceiveContentListener(view, mimeTypes, listener);
+            return;
+        }
         mimeTypes = (mimeTypes == null || mimeTypes.length == 0) ? null : mimeTypes;
         if (listener != null) {
             Preconditions.checkArgument(mimeTypes != null,
@@ -2794,6 +2799,9 @@
      */
     @Nullable
     public static String[] getOnReceiveContentMimeTypes(@NonNull View view) {
+        if (Build.VERSION.SDK_INT >= 31) {
+            return Api31Impl.getReceiveContentMimeTypes(view);
+        }
         return (String[]) view.getTag(R.id.tag_on_receive_content_mime_types);
     }
 
@@ -2821,6 +2829,9 @@
             Log.d(TAG, "performReceiveContent: " + payload
                     + ", view=" + view.getClass().getSimpleName() + "[" + view.getId() + "]");
         }
+        if (Build.VERSION.SDK_INT >= 31) {
+            return Api31Impl.performReceiveContent(view, payload);
+        }
         OnReceiveContentListener listener =
                 (OnReceiveContentListener) view.getTag(R.id.tag_on_receive_content_listener);
         if (listener != null) {
@@ -2840,6 +2851,71 @@
     private static final OnReceiveContentViewBehavior NO_OP_ON_RECEIVE_CONTENT_VIEW_BEHAVIOR =
             payload -> payload;
 
+    @RequiresApi(31)
+    private static final class Api31Impl {
+        private Api31Impl() {}
+
+        @DoNotInline
+        public static void setOnReceiveContentListener(@NonNull View view,
+                @Nullable String[] mimeTypes, @Nullable final OnReceiveContentListener listener) {
+            if (listener == null) {
+                view.setOnReceiveContentListener(mimeTypes, null);
+            } else {
+                view.setOnReceiveContentListener(mimeTypes,
+                        new OnReceiveContentListenerAdapter(listener));
+            }
+        }
+
+        @DoNotInline
+        @Nullable
+        public static String[] getReceiveContentMimeTypes(@NonNull View view) {
+            return view.getReceiveContentMimeTypes();
+        }
+
+        @DoNotInline
+        @Nullable
+        public static ContentInfoCompat performReceiveContent(@NonNull View view,
+                @NonNull ContentInfoCompat payload) {
+            ContentInfo platPayload = payload.toContentInfo();
+            ContentInfo platResult = view.performReceiveContent(platPayload);
+            if (platResult == null) {
+                return null;
+            }
+            if (platResult == platPayload) {
+                // Avoid unnecessary conversion when returning the original payload unchanged.
+                return payload;
+            }
+            return ContentInfoCompat.toContentInfoCompat(platResult);
+        }
+    }
+
+    @RequiresApi(31)
+    private static final class OnReceiveContentListenerAdapter implements
+            android.view.OnReceiveContentListener {
+
+        @NonNull
+        private final OnReceiveContentListener mJetpackListener;
+
+        OnReceiveContentListenerAdapter(@NonNull OnReceiveContentListener jetpackListener) {
+            mJetpackListener = jetpackListener;
+        }
+
+        @Nullable
+        @Override
+        public ContentInfo onReceiveContent(@NonNull View view, @NonNull ContentInfo platPayload) {
+            ContentInfoCompat payload = ContentInfoCompat.toContentInfoCompat(platPayload);
+            ContentInfoCompat result = mJetpackListener.onReceiveContent(view, payload);
+            if (result == null) {
+                return null;
+            }
+            if (result == payload) {
+                // Avoid unnecessary conversion when returning the original payload unchanged.
+                return platPayload;
+            }
+            return result.toContentInfo();
+        }
+    }
+
     /**
      * Controls whether the entire hierarchy under this view will save its
      * state when a state saving traversal occurs from its parent.
diff --git a/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java b/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
index 4bea6a5..90fbfcb 100644
--- a/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
@@ -18,9 +18,14 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.os.Build;
+import android.util.AttributeSet;
 import android.widget.EdgeEffect;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
 
 /**
  * Helper for accessing {@link android.widget.EdgeEffect}.
@@ -41,7 +46,8 @@
      *
      * @param context Context to use for theming the effect
      *
-     * @deprecated Use {@link EdgeEffect} constructor directly.
+     * @deprecated Use {@link EdgeEffect} constructor directly or
+     * {@link EdgeEffectCompat#create(Context, AttributeSet)}.
      */
     @Deprecated
     public EdgeEffectCompat(Context context) {
@@ -49,6 +55,42 @@
     }
 
     /**
+     * Constructs and returns a new EdgeEffect themed using the given context, allowing support
+     * for the view attributes.
+     *
+     * @param context Context to use for theming the effect
+     * @param attrs The attributes of the XML tag that is inflating the view
+     */
+    @NonNull
+    public static EdgeEffect create(@NonNull Context context, @Nullable AttributeSet attrs) {
+        if (BuildCompat.isAtLeastS()) {
+            return Api31Impl.create(context, attrs);
+        }
+
+        return new EdgeEffect(context);
+    }
+
+    /**
+     * Returns the pull distance needed to be released to remove the showing effect.
+     * It is determined by the {@link #onPull(float, float)} <code>deltaDistance</code> and
+     * any animating values, including from {@link #onAbsorb(int)} and {@link #onRelease()}.
+     *
+     * This can be used in conjunction with {@link #onPullDistance(EdgeEffect, float, float)} to
+     * release the currently showing effect.
+     *
+     * On {@link Build.VERSION_CODES#R} and earlier, this will return 0.
+     *
+     * @return The pull distance that must be released to remove the showing effect or 0 for
+     * versions {@link Build.VERSION_CODES#R} and earlier.
+     */
+    public static float getDistance(@NonNull EdgeEffect edgeEffect) {
+        if (BuildCompat.isAtLeastS()) {
+            return Api31Impl.getDistance(edgeEffect);
+        }
+        return 0;
+    }
+
+    /**
      * Set the size of this edge effect in pixels.
      *
      * @param width Effect width in pixels
@@ -157,6 +199,51 @@
     }
 
     /**
+     * A view should call this when content is pulled away from an edge by the user.
+     * This will update the state of the current visual effect and its associated animation.
+     * The host view should always {@link android.view.View#invalidate()} after this
+     * and draw the results accordingly. This works similarly to {@link #onPull(float, float)},
+     * but returns the amount of <code>deltaDistance</code> that has been consumed. For versions
+     * {@link Build.VERSION_CODES#S} and above, if the {@link #getDistance(EdgeEffect)} is currently
+     * 0 and <code>deltaDistance</code> is negative, this function will return 0 and the drawn value
+     * will remain unchanged. For versions {@link Build.VERSION_CODES#R} and below, this will
+     * consume all of the provided value and return <code>deltaDistance</code>.
+     *
+     * This method can be used to reverse the effect from a pull or absorb and partially consume
+     * some of a motion:
+     *
+     * <pre class="prettyprint">
+     *     if (deltaY < 0 && EdgeEffectCompat.getDistance(edgeEffect) != 0) {
+     *         float displacement = x / getWidth();
+     *         float dist = deltaY / getHeight();
+     *         float consumed = EdgeEffectCompat.onPullDistance(edgeEffect, dist, displacement);
+     *         deltaY -= consumed * getHeight();
+     *         if (edgeEffect.getDistance() == 0f) edgeEffect.onRelease();
+     *     }
+     * </pre>
+     *
+     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+     *                      1.f (full length of the view) or negative values to express change
+     *                      back toward the edge reached to initiate the effect.
+     * @param displacement The displacement from the starting side of the effect of the point
+     *                     initiating the pull. In the case of touch this is the finger position.
+     *                     Values may be from 0-1.
+     * @return The amount of <code>deltaDistance</code> that was consumed, a number between
+     * 0 and <code>deltaDistance</code>.
+     */
+    public static float onPullDistance(
+            @NonNull EdgeEffect edgeEffect,
+            float deltaDistance,
+            float displacement
+    ) {
+        if (BuildCompat.isAtLeastS()) {
+            return Api31Impl.onPullDistance(edgeEffect, deltaDistance, displacement);
+        }
+        onPull(edgeEffect, deltaDistance, displacement);
+        return deltaDistance;
+    }
+
+    /**
      * Call when the object is released after being pulled.
      * This will begin the "decay" phase of the effect. After calling this method
      * the host view should {@link android.view.View#invalidate()} if this method
@@ -207,4 +294,42 @@
     public boolean draw(Canvas canvas) {
         return mEdgeEffect.draw(canvas);
     }
+
+    // TODO(b/181171227): This actually requires S, but we don't have a version for S yet.
+    @RequiresApi(Build.VERSION_CODES.R)
+    private static class Api31Impl {
+        private Api31Impl() {}
+
+        @DoNotInline
+        public static EdgeEffect create(Context context, AttributeSet attrs) {
+            try {
+                return new EdgeEffect(context, attrs);
+            } catch (Throwable t) {
+                return new EdgeEffect(context); // Old preview release
+            }
+        }
+
+        @DoNotInline
+        public static float onPullDistance(
+                EdgeEffect edgeEffect,
+                float deltaDistance,
+                float displacement
+        ) {
+            try {
+                return edgeEffect.onPullDistance(deltaDistance, displacement);
+            } catch (Throwable t) {
+                edgeEffect.onPull(deltaDistance, displacement); // Old preview release
+                return 0;
+            }
+        }
+
+        @DoNotInline
+        public static float getDistance(EdgeEffect edgeEffect) {
+            try {
+                return edgeEffect.getDistance();
+            } catch (Throwable t) {
+                return 0; // Old preview release
+            }
+        }
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index ab9cc52..ab3531c 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -17,6 +17,7 @@
 
 package androidx.core.widget;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
 import android.content.Context;
@@ -48,6 +49,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.R;
 import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.core.view.InputDeviceCompat;
@@ -102,8 +104,18 @@
 
     private final Rect mTempRect = new Rect();
     private OverScroller mScroller;
-    private EdgeEffect mEdgeGlowTop;
-    private EdgeEffect mEdgeGlowBottom;
+
+    /** @hide */
+    @RestrictTo(LIBRARY)
+    @VisibleForTesting
+    @NonNull
+    public EdgeEffect mEdgeGlowTop;
+
+    /** @hide */
+    @RestrictTo(LIBRARY)
+    @VisibleForTesting
+    @NonNull
+    public EdgeEffect mEdgeGlowBottom;
 
     /**
      * Position of the last motion event.
@@ -198,6 +210,9 @@
     public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        mEdgeGlowTop = EdgeEffectCompat.create(context, attrs);
+        mEdgeGlowBottom = EdgeEffectCompat.create(context, attrs);
+
         initScrollView();
 
         final TypedArray a = context.obtainStyledAttributes(
@@ -775,7 +790,7 @@
             case MotionEvent.ACTION_DOWN: {
                 final int y = (int) ev.getY();
                 if (!inChild((int) ev.getX(), y)) {
-                    mIsBeingDragged = false;
+                    mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished();
                     recycleVelocityTracker();
                     break;
                 }
@@ -792,11 +807,12 @@
                 /*
                  * If being flinged and user touches the screen, initiate drag;
                  * otherwise don't. mScroller.isFinished should be false when
-                 * being flinged. We need to call computeScrollOffset() first so that
+                 * being flinged. We also want to catch the edge glow and start dragging
+                 * if one is being animated. We need to call computeScrollOffset() first so that
                  * isFinished() is correct.
                 */
                 mScroller.computeScrollOffset();
-                mIsBeingDragged = !mScroller.isFinished();
+                mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished();
                 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                 break;
             }
@@ -842,7 +858,7 @@
                 if (getChildCount() == 0) {
                     return false;
                 }
-                if ((mIsBeingDragged = !mScroller.isFinished())) {
+                if (mIsBeingDragged) {
                     final ViewParent parent = getParent();
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
@@ -872,6 +888,7 @@
 
                 final int y = (int) ev.getY(activePointerIndex);
                 int deltaY = mLastMotionY - y;
+                deltaY -= releaseVerticalGlow(deltaY, ev.getX(activePointerIndex));
                 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                     final ViewParent parent = getParent();
                     if (parent != null) {
@@ -903,11 +920,9 @@
 
                     // Calling overScrollByCompat will call onOverScrolled, which
                     // calls onScrollChanged if applicable.
-                    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
-                            0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
-                        // Break our velocity if we hit a scroll barrier.
-                        mVelocityTracker.clear();
-                    }
+                    boolean clearVelocityTracker =
+                            overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
+                                    0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
 
                     final int scrolledDeltaY = getScrollY() - oldY;
                     final int unconsumedY = deltaY - scrolledDeltaY;
@@ -922,27 +937,31 @@
 
                     if (canOverscroll) {
                         deltaY -= mScrollConsumed[1];
-                        ensureGlows();
                         final int pulledToY = oldY + deltaY;
                         if (pulledToY < 0) {
-                            EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
+                            EdgeEffectCompat.onPullDistance(mEdgeGlowTop,
+                                    (float) -deltaY / getHeight(),
                                     ev.getX(activePointerIndex) / getWidth());
                             if (!mEdgeGlowBottom.isFinished()) {
                                 mEdgeGlowBottom.onRelease();
                             }
                         } else if (pulledToY > range) {
-                            EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
-                                    1.f - ev.getX(activePointerIndex)
-                                            / getWidth());
+                            EdgeEffectCompat.onPullDistance(mEdgeGlowBottom,
+                                    (float) deltaY / getHeight(),
+                                    1.f - ev.getX(activePointerIndex) / getWidth());
                             if (!mEdgeGlowTop.isFinished()) {
                                 mEdgeGlowTop.onRelease();
                             }
                         }
-                        if (mEdgeGlowTop != null
-                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
+                        if (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished()) {
                             ViewCompat.postInvalidateOnAnimation(this);
+                            clearVelocityTracker = false;
                         }
                     }
+                    if (clearVelocityTracker) {
+                        // Break our velocity if we hit a scroll barrier.
+                        mVelocityTracker.clear();
+                    }
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -950,7 +969,8 @@
                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                 if ((Math.abs(initialVelocity) >= mMinimumVelocity)) {
-                    if (!dispatchNestedPreFling(0, -initialVelocity)) {
+                    if (!edgeEffectFling(initialVelocity)
+                            && !dispatchNestedPreFling(0, -initialVelocity)) {
                         dispatchNestedFling(0, -initialVelocity, true);
                         fling(-initialVelocity);
                     }
@@ -991,6 +1011,42 @@
         return true;
     }
 
+    private boolean edgeEffectFling(int velocityY) {
+        boolean consumed = true;
+        if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) {
+            mEdgeGlowTop.onAbsorb(velocityY);
+        } else if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) {
+            mEdgeGlowBottom.onAbsorb(-velocityY);
+        } else {
+            consumed = false;
+        }
+        return consumed;
+    }
+
+    /**
+     * This stops any edge glow animation that is currently running by applying a
+     * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices,
+     * this method does nothing, allowing any animating edge effect to continue animating and
+     * returning <code>false</code> always.
+     *
+     * @param e The motion event to use to indicate the finger position for the displacement of
+     *          the current pull.
+     * @return <code>true</code> if any edge effect had an existing effect to be drawn ond the
+     * animation was stopped or <code>false</code> if no edge effect had a value to display.
+     */
+    private boolean stopGlowAnimations(MotionEvent e) {
+        boolean stopped = false;
+        if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) {
+            EdgeEffectCompat.onPullDistance(mEdgeGlowTop, 0, e.getY() / getHeight());
+            stopped = true;
+        }
+        if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) {
+            EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, 0, 1 - e.getY() / getHeight());
+            stopped = true;
+        }
+        return stopped;
+    }
+
     private void onSecondaryPointerUp(MotionEvent ev) {
         final int pointerIndex = ev.getActionIndex();
         final int pointerId = ev.getPointerId(pointerIndex);
@@ -1639,7 +1695,6 @@
             final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS
                     || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
             if (canOverscroll) {
-                ensureGlows();
                 if (unconsumed < 0) {
                     if (mEdgeGlowTop.isFinished()) {
                         mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
@@ -1660,6 +1715,40 @@
         }
     }
 
+    /**
+     * If either of the vertical edge glows are currently active, this consumes part or all of
+     * deltaY on the edge glow.
+     *
+     * @param deltaY The pointer motion, in pixels, in the vertical direction, positive
+     *                         for moving down and negative for moving up.
+     * @param x The vertical position of the pointer.
+     * @return The amount of <code>deltaY</code> that has been consumed by the
+     * edge glow.
+     */
+    private int releaseVerticalGlow(int deltaY, float x) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = x / getWidth();
+        float pullDistance = (float) deltaY / getHeight();
+        if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mEdgeGlowTop, -pullDistance, displacement);
+            if (EdgeEffectCompat.getDistance(mEdgeGlowTop) == 0) {
+                mEdgeGlowTop.onRelease();
+            }
+        } else if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, pullDistance,
+                    1 - displacement);
+            if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) == 0) {
+                mEdgeGlowBottom.onRelease();
+            }
+        }
+        int pixelsConsumed = Math.round(consumed * getHeight());
+        if (pixelsConsumed != 0) {
+            invalidate();
+        }
+        return pixelsConsumed;
+    }
+
     private void runAnimatedScroll(boolean participateInNestedScrolling) {
         if (participateInNestedScrolling) {
             startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
@@ -1952,10 +2041,8 @@
         recycleVelocityTracker();
         stopNestedScroll(ViewCompat.TYPE_TOUCH);
 
-        if (mEdgeGlowTop != null) {
-            mEdgeGlowTop.onRelease();
-            mEdgeGlowBottom.onRelease();
-        }
+        mEdgeGlowTop.onRelease();
+        mEdgeGlowBottom.onRelease();
     }
 
     /**
@@ -1981,67 +2068,52 @@
         }
     }
 
-    private void ensureGlows() {
-        if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
-            if (mEdgeGlowTop == null) {
-                Context context = getContext();
-                mEdgeGlowTop = new EdgeEffect(context);
-                mEdgeGlowBottom = new EdgeEffect(context);
-            }
-        } else {
-            mEdgeGlowTop = null;
-            mEdgeGlowBottom = null;
-        }
-    }
-
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
-        if (mEdgeGlowTop != null) {
-            final int scrollY = getScrollY();
-            if (!mEdgeGlowTop.isFinished()) {
-                final int restoreCount = canvas.save();
-                int width = getWidth();
-                int height = getHeight();
-                int xTranslation = 0;
-                int yTranslation = Math.min(0, scrollY);
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
-                    width -= getPaddingLeft() + getPaddingRight();
-                    xTranslation += getPaddingLeft();
-                }
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
-                    height -= getPaddingTop() + getPaddingBottom();
-                    yTranslation += getPaddingTop();
-                }
-                canvas.translate(xTranslation, yTranslation);
-                mEdgeGlowTop.setSize(width, height);
-                if (mEdgeGlowTop.draw(canvas)) {
-                    ViewCompat.postInvalidateOnAnimation(this);
-                }
-                canvas.restoreToCount(restoreCount);
+        final int scrollY = getScrollY();
+        if (!mEdgeGlowTop.isFinished()) {
+            final int restoreCount = canvas.save();
+            int width = getWidth();
+            int height = getHeight();
+            int xTranslation = 0;
+            int yTranslation = Math.min(0, scrollY);
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
+                width -= getPaddingLeft() + getPaddingRight();
+                xTranslation += getPaddingLeft();
             }
-            if (!mEdgeGlowBottom.isFinished()) {
-                final int restoreCount = canvas.save();
-                int width = getWidth();
-                int height = getHeight();
-                int xTranslation = 0;
-                int yTranslation = Math.max(getScrollRange(), scrollY) + height;
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
-                    width -= getPaddingLeft() + getPaddingRight();
-                    xTranslation += getPaddingLeft();
-                }
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
-                    height -= getPaddingTop() + getPaddingBottom();
-                    yTranslation -= getPaddingBottom();
-                }
-                canvas.translate(xTranslation - width, yTranslation);
-                canvas.rotate(180, width, 0);
-                mEdgeGlowBottom.setSize(width, height);
-                if (mEdgeGlowBottom.draw(canvas)) {
-                    ViewCompat.postInvalidateOnAnimation(this);
-                }
-                canvas.restoreToCount(restoreCount);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
+                height -= getPaddingTop() + getPaddingBottom();
+                yTranslation += getPaddingTop();
             }
+            canvas.translate(xTranslation, yTranslation);
+            mEdgeGlowTop.setSize(width, height);
+            if (mEdgeGlowTop.draw(canvas)) {
+                ViewCompat.postInvalidateOnAnimation(this);
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+        if (!mEdgeGlowBottom.isFinished()) {
+            final int restoreCount = canvas.save();
+            int width = getWidth();
+            int height = getHeight();
+            int xTranslation = 0;
+            int yTranslation = Math.max(getScrollRange(), scrollY) + height;
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
+                width -= getPaddingLeft() + getPaddingRight();
+                xTranslation += getPaddingLeft();
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
+                height -= getPaddingTop() + getPaddingBottom();
+                yTranslation -= getPaddingBottom();
+            }
+            canvas.translate(xTranslation - width, yTranslation);
+            canvas.rotate(180, width, 0);
+            mEdgeGlowBottom.setSize(width, height);
+            if (mEdgeGlowBottom.draw(canvas)) {
+                ViewCompat.postInvalidateOnAnimation(this);
+            }
+            canvas.restoreToCount(restoreCount);
         }
     }
 
diff --git a/core/core/src/main/res/values/attrs.xml b/core/core/src/main/res/values/attrs.xml
index 5c644f0..45ab302 100644
--- a/core/core/src/main/res/values/attrs.xml
+++ b/core/core/src/main/res/values/attrs.xml
@@ -110,6 +110,9 @@
         <!-- Alpha multiplier applied to the base color. -->
         <attr name="alpha" format="float" />
         <attr name="android:alpha"/>
+        <!-- Perceptual luminance applied to the base color. From 0 to 100. -->
+        <attr name="lStar" format="float" />
+        <attr name="android:lStar" />
     </declare-styleable>
 
     <!-- Used to describe the gradient for fill or stroke in a path of VectorDrawable. -->
diff --git a/core/core/src/main/res/values/public.xml b/core/core/src/main/res/values/public.xml
index 72fa773..9b344c2 100644
--- a/core/core/src/main/res/values/public.xml
+++ b/core/core/src/main/res/values/public.xml
@@ -18,6 +18,7 @@
 <resources>
     <!-- Definitions of attributes to be exposed as public -->
     <public type="attr" name="alpha"/>
+    <public type="attr" name="lStar" />
     <public type="attr" name="fontProviderAuthority"/>
     <public type="attr" name="fontProviderPackage"/>
     <public type="attr" name="fontProviderQuery"/>
diff --git a/development/studio/idea.properties b/development/studio/idea.properties
index fd64fdb..bd80f7b 100644
--- a/development/studio/idea.properties
+++ b/development/studio/idea.properties
@@ -5,12 +5,12 @@
 #---------------------------------------------------------------------
 # Uncomment this option if you want to customize path to IDE config folder. Make sure you're using forward slashes.
 #---------------------------------------------------------------------
-idea.config.path=${user.home}/.AndroidStudioAndroidX/config
+idea.config.path=${user.home}/.AndroidStudioAndroidXPlatform/config
 
 #---------------------------------------------------------------------
 # Uncomment this option if you want to customize path to IDE system folder. Make sure you're using forward slashes.
 #---------------------------------------------------------------------
-idea.system.path=${user.home}/.AndroidStudioAndroidX/system
+idea.system.path=${user.home}/.AndroidStudioAndroidXPlatform/system
 
 #---------------------------------------------------------------------
 # Uncomment this option if you want to customize path to user installed plugins folder. Make sure you're using forward slashes.
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 2baef92..67c7ce5 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -98,6 +98,8 @@
     docs(project(":core:core-appdigest"))
     docs(project(":core:core-google-shortcuts"))
     docs(project(":core:core-ktx"))
+    docs(project(":core:core-remoteviews"))
+    docs(project(":core:core-splashscreen"))
     docs(project(":core:core-role"))
     docs(project(":cursoradapter:cursoradapter"))
     docs(project(":customview:customview"))
diff --git a/gradle.properties b/gradle.properties
index adcf1fd..58e898a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,7 +18,9 @@
 kotlin.mpp.stability.nowarn=true
 # Workaround for b/141364941
 android.forceJacocoOutOfProcess=true
-androidx.writeVersionedApiFiles=true
+
+# Don't generate versioned API files
+androidx.writeVersionedApiFiles=false
 
 # Disable features we do not use
 android.defaults.buildfeatures.aidl=false
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index 9cceb94..bac6294 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -1041,6 +1041,10 @@
       "to": "android/support/v4/widget/annotations"
     },
     {
+      "from": "androidx/viewpager/widget/annotations",
+      "to": "android/support/v4/view/annotations"
+    },
+    {
       "from": "androidx/annotation/experimental/(.*)",
       "to": "ignore"
     },
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index 657d48e..8738cc7 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -10,7 +10,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.core:core:1.3.2")
+    api project(":core:core")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.customview:customview:1.0.0")
 
@@ -24,6 +24,7 @@
     androidTestImplementation(libs.truth)
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.multidex)
     androidTestImplementation(project(":internal-testutils-espresso"))
     androidTestImplementation(project(":internal-testutils-runtime"))
     androidTestImplementation(project(":internal-testutils-common"))
@@ -47,6 +48,7 @@
     defaultConfig {
         multiDexEnabled = true
         testInstrumentationRunner "androidx.testutils.ActivityRecyclingAndroidJUnitRunner"
+        multiDexEnabled true
     }
 }
 
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java
index 0153bb90..495e073 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java
@@ -84,6 +84,7 @@
         assertNull(factory.mBottom);
         assertNotNull(factory.mTop);
         assertTrue(factory.mTop.mPullDistance > 0);
+        scrollViewBy(-3);
 
         scrollToPosition(NUM_ITEMS - 1);
         waitForIdleScroll(mRecyclerView);
@@ -144,6 +145,7 @@
     private class TestEdgeEffect extends EdgeEffect {
 
         private float mPullDistance;
+        private float mDistance;
 
         TestEdgeEffect(Context context) {
             super(context);
@@ -157,6 +159,19 @@
         @Override
         public void onPull(float deltaDistance) {
             mPullDistance = deltaDistance;
+            mDistance += deltaDistance;
+        }
+
+        @Override
+        public float onPullDistance(float deltaDistance, float displacement) {
+            float maxDelta = Math.max(-mDistance, deltaDistance);
+            onPull(maxDelta);
+            return maxDelta;
+        }
+
+        @Override
+        public float getDistance() {
+            return mDistance;
         }
     }
 }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingChildTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingChildTest.java
index 5a27004..b26c074 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingChildTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingChildTest.java
@@ -80,6 +80,7 @@
                 ApplicationProvider.getApplicationContext()).getScaledTouchSlop();
 
         mRecyclerView = new RecyclerView(context);
+        mRecyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
         mRecyclerView.setMinimumWidth(1000);
         mRecyclerView.setMinimumHeight(1000);
 
@@ -663,4 +664,5 @@
             super(itemView);
         }
     }
+
 }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StretchEdgeEffectTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StretchEdgeEffectTest.java
new file mode 100644
index 0000000..c2a6ee2
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StretchEdgeEffectTest.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright 2021 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 androidx.recyclerview.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Build;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.EdgeEffect;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.InputDeviceCompat;
+import androidx.core.widget.EdgeEffectCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class StretchEdgeEffectTest extends BaseRecyclerViewInstrumentationTest {
+    private static final int NUM_ITEMS = 10;
+
+    private RecyclerView mRecyclerView;
+    private LinearLayoutManager mLayoutManager;
+
+    @Before
+    public void setup() throws Throwable {
+        mLayoutManager = new LinearLayoutManager(getActivity());
+        mLayoutManager.ensureLayoutState();
+
+        mRecyclerView = new RecyclerView(getActivity());
+        mRecyclerView.setLayoutManager(mLayoutManager);
+        mRecyclerView.setAdapter(new TestAdapter(NUM_ITEMS) {
+
+            @Override
+            public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+                    int viewType) {
+                TestViewHolder holder = super.onCreateViewHolder(parent, viewType);
+                holder.itemView.setMinimumHeight(mRecyclerView.getMeasuredHeight() * 2 / NUM_ITEMS);
+                holder.itemView.setMinimumWidth(mRecyclerView.getMeasuredWidth() * 2 / NUM_ITEMS);
+                return holder;
+            }
+        });
+        setRecyclerView(mRecyclerView);
+        getInstrumentation().waitForIdleSync();
+        assertThat("Assumption check", mRecyclerView.getChildCount() > 0, is(true));
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testLeftEdgeEffectRetract() throws Throwable {
+        mActivityRule.runOnUiThread(
+                () -> mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL));
+        TestEdgeEffectFactory
+                factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollHorizontalBy(-3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mLeft) > 0);
+        }
+        scrollHorizontalBy(4);
+        assertEquals(0f, EdgeEffectCompat.getDistance(factory.mLeft), 0f);
+        if (isSOrHigher()) {
+            assertTrue(factory.mLeft.isFinished());
+        }
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testTopEdgeEffectRetract() throws Throwable {
+        TestEdgeEffectFactory
+                factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mTop) > 0);
+        }
+        scrollVerticalBy(-4);
+        assertEquals(0f, EdgeEffectCompat.getDistance(factory.mTop), 0f);
+        if (isSOrHigher()) {
+            assertTrue(factory.mTop.isFinished());
+        }
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testRightEdgeEffectRetract() throws Throwable {
+        mActivityRule.runOnUiThread(
+                () -> mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL));
+        TestEdgeEffectFactory
+                factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(NUM_ITEMS - 1);
+        waitForIdleScroll(mRecyclerView);
+        scrollHorizontalBy(3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mRight) > 0);
+        }
+        scrollHorizontalBy(-4);
+        assertEquals(0f, EdgeEffectCompat.getDistance(factory.mRight), 0f);
+        if (isSOrHigher()) {
+            assertTrue(factory.mRight.isFinished());
+        }
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testBottomEdgeEffectRetract() throws Throwable {
+        TestEdgeEffectFactory factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(NUM_ITEMS - 1);
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(-3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mBottom) > 0);
+        }
+
+        scrollVerticalBy(4);
+        if (isSOrHigher()) {
+            assertEquals(0f, EdgeEffectCompat.getDistance(factory.mBottom), 0f);
+            assertTrue(factory.mBottom.isFinished());
+        }
+    }
+
+    /**
+     * A fling should be allowed during pull, but only for and earlier
+     */
+    @Test
+    public void testFlingAfterStretchLeft() throws Throwable {
+        mActivityRule.runOnUiThread(
+                () -> mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL));
+        CaptureOnAbsorbFactory factory = new CaptureOnAbsorbFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollHorizontalBy(-3);
+
+        if (isSOrHigher()) {
+            // test flinging right
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mLeft);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(-1000, 0));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mLeft), 0.01f);
+                assertEquals(1000, factory.mLeft.mAbsorbVelocity);
+                // reset the edge effect
+                factory.mLeft.finish();
+            });
+
+            scrollHorizontalBy(-3);
+
+            // test flinging left
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mLeft);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(1000, 0));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mLeft), 0.01f);
+                assertEquals(-1000, factory.mLeft.mAbsorbVelocity);
+            });
+        } else {
+            // fling left and it should just scroll
+            mActivityRule.runOnUiThread(() -> {
+                assertEquals(0, mLayoutManager.findFirstVisibleItemPosition());
+                assertTrue(mRecyclerView.fling(5000, 0));
+                assertEquals(0, factory.mLeft.mAbsorbVelocity);
+            });
+            waitForIdleScroll(mRecyclerView);
+            mActivityRule.runOnUiThread(() -> {
+                assertTrue(mLayoutManager.findFirstVisibleItemPosition() > 0);
+            });
+        }
+    }
+
+    /**
+     * A fling should be allowed during pull.
+     */
+    @Test
+    public void testFlingAfterStretchTop() throws Throwable {
+        CaptureOnAbsorbFactory factory = new CaptureOnAbsorbFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(3);
+
+        if (isSOrHigher()) {
+            // test flinging down
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mTop);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(0, -1000));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mTop), 0.01f);
+                assertEquals(1000, factory.mTop.mAbsorbVelocity);
+                // reset the edge effect
+                factory.mTop.finish();
+            });
+
+            scrollVerticalBy(3);
+
+            // test flinging up
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mTop);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(0, 1000));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mTop), 0.01f);
+                assertEquals(-1000, factory.mTop.mAbsorbVelocity);
+            });
+        } else {
+            // fling up and it should just scroll
+            mActivityRule.runOnUiThread(() -> {
+                assertEquals(0, mLayoutManager.findFirstVisibleItemPosition());
+                assertTrue(mRecyclerView.fling(0, 5000));
+                assertEquals(0, factory.mTop.mAbsorbVelocity);
+            });
+            waitForIdleScroll(mRecyclerView);
+            mActivityRule.runOnUiThread(() -> {
+                assertTrue(mLayoutManager.findFirstVisibleItemPosition() > 0);
+            });
+        }
+    }
+
+    /**
+     * A fling should be allowed during pull.
+     */
+    @Test
+    public void testFlingAfterStretchRight() throws Throwable {
+        mActivityRule.runOnUiThread(
+                () -> mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL));
+        CaptureOnAbsorbFactory factory = new CaptureOnAbsorbFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(NUM_ITEMS - 1);
+        waitForIdleScroll(mRecyclerView);
+        scrollHorizontalBy(3);
+
+        if (isSOrHigher()) {
+            // test flinging left
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mRight);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(1000, 0));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mRight), 0.01f);
+                assertEquals(1000, factory.mRight.mAbsorbVelocity);
+                // reset the edge effect
+                factory.mRight.finish();
+            });
+
+            scrollHorizontalBy(3);
+
+            // test flinging right
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mRight);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(-1000, 0));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mRight), 0.01f);
+                assertEquals(-1000, factory.mRight.mAbsorbVelocity);
+            });
+        } else {
+            // fling right and it should just scroll
+            mActivityRule.runOnUiThread(() -> {
+                assertEquals(mRecyclerView.getAdapter().getItemCount() - 1,
+                        mLayoutManager.findLastVisibleItemPosition());
+                assertTrue(mRecyclerView.fling(-5000, 0));
+                assertEquals(0, factory.mRight.mAbsorbVelocity);
+            });
+            waitForIdleScroll(mRecyclerView);
+            mActivityRule.runOnUiThread(() -> {
+                assertTrue(mLayoutManager.findLastVisibleItemPosition()
+                        < mRecyclerView.getAdapter().getItemCount() - 1);
+            });
+
+        }
+    }
+
+    /**
+     * A fling should be allowed during pull.
+     */
+    @Test
+    public void testFlingAfterStretchBottom() throws Throwable {
+        CaptureOnAbsorbFactory factory = new CaptureOnAbsorbFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(NUM_ITEMS - 1);
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(-3);
+
+        if (isSOrHigher()) {
+            // test flinging up
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mBottom);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(0, 1000));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mBottom), 0.01f);
+                assertEquals(1000, factory.mBottom.mAbsorbVelocity);
+                // reset the edge effect
+                factory.mBottom.finish();
+            });
+
+            scrollVerticalBy(-3);
+
+            // test flinging down
+            mActivityRule.runOnUiThread(() -> {
+                float pullDistance = EdgeEffectCompat.getDistance(factory.mBottom);
+                assertTrue(pullDistance > 0);
+                assertTrue(mRecyclerView.fling(0, -1000));
+                assertEquals(pullDistance, EdgeEffectCompat.getDistance(factory.mBottom), 0.01f);
+                assertEquals(-1000, factory.mBottom.mAbsorbVelocity);
+            });
+        } else {
+            // fling up and it should just scroll
+            mActivityRule.runOnUiThread(() -> {
+                assertEquals(mRecyclerView.getAdapter().getItemCount() - 1,
+                        mLayoutManager.findLastVisibleItemPosition());
+                assertTrue(mRecyclerView.fling(0, -5000));
+                assertEquals(0, factory.mBottom.mAbsorbVelocity);
+            });
+            waitForIdleScroll(mRecyclerView);
+            mActivityRule.runOnUiThread(() -> {
+                assertTrue(mLayoutManager.findLastVisibleItemPosition()
+                        < mRecyclerView.getAdapter().getItemCount() - 1);
+            });
+        }
+    }
+
+    private static boolean isSOrHigher() {
+        // TODO(b/181171227): Simplify this
+        int sdk = Build.VERSION.SDK_INT;
+        return sdk > Build.VERSION_CODES.R
+                || (sdk == Build.VERSION_CODES.R && Build.VERSION.PREVIEW_SDK_INT != 0);
+    }
+
+    private void scrollVerticalBy(final int value) throws Throwable {
+        mActivityRule.runOnUiThread(() -> TouchUtils.scrollView(MotionEvent.AXIS_VSCROLL, value,
+                InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView));
+    }
+
+    private void scrollHorizontalBy(final int value) throws Throwable {
+        mActivityRule.runOnUiThread(() -> TouchUtils.scrollView(MotionEvent.AXIS_HSCROLL, value,
+                InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView));
+    }
+
+    private class TestEdgeEffectFactory extends RecyclerView.EdgeEffectFactory {
+        TestEdgeEffect mTop, mBottom, mLeft, mRight;
+
+        @NonNull
+        @Override
+        protected EdgeEffect createEdgeEffect(RecyclerView view, int direction) {
+            TestEdgeEffect effect = new TestEdgeEffect(view.getContext());
+            switch (direction) {
+                case DIRECTION_LEFT:
+                    mLeft = effect;
+                    break;
+                case DIRECTION_TOP:
+                    mTop = effect;
+                    break;
+                case DIRECTION_RIGHT:
+                    mRight = effect;
+                    break;
+                case DIRECTION_BOTTOM:
+                    mBottom = effect;
+                    break;
+            }
+            return effect;
+        }
+    }
+
+    private class TestEdgeEffect extends EdgeEffect {
+
+        private float mDistance;
+
+        TestEdgeEffect(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onPull(float deltaDistance, float displacement) {
+            onPull(deltaDistance);
+        }
+
+        @Override
+        public void onPull(float deltaDistance) {
+            mDistance += deltaDistance;
+        }
+
+        @Override
+        public float onPullDistance(float deltaDistance, float displacement) {
+            float maxDelta = Math.max(-mDistance, deltaDistance);
+            onPull(maxDelta);
+            return maxDelta;
+        }
+
+        @Override
+        public float getDistance() {
+            return mDistance;
+        }
+    }
+
+    private class CaptureOnAbsorbFactory extends RecyclerView.EdgeEffectFactory {
+        CaptureOnAbsorb mTop, mBottom, mLeft, mRight;
+
+        @NonNull
+        @Override
+        protected EdgeEffect createEdgeEffect(RecyclerView view, int direction) {
+            CaptureOnAbsorb effect = new CaptureOnAbsorb(view.getContext());
+            switch (direction) {
+                case DIRECTION_LEFT:
+                    mLeft = effect;
+                    break;
+                case DIRECTION_TOP:
+                    mTop = effect;
+                    break;
+                case DIRECTION_RIGHT:
+                    mRight = effect;
+                    break;
+                case DIRECTION_BOTTOM:
+                    mBottom = effect;
+                    break;
+            }
+            return effect;
+        }
+    }
+
+    private static class CaptureOnAbsorb extends EdgeEffect {
+        public int mAbsorbVelocity;
+
+        CaptureOnAbsorb(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onAbsorb(int velocity) {
+            super.onAbsorb(velocity);
+            mAbsorbVelocity = velocity;
+        }
+
+    }
+}
diff --git a/recyclerview/recyclerview/src/androidTest/res/layout/stretch_rv.xml b/recyclerview/recyclerview/src/androidTest/res/layout/stretch_rv.xml
new file mode 100644
index 0000000..133ae7b
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/res/layout/stretch_rv.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2021 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.
+  -->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/recycler_view"
+    android:layout_width="90px"
+    android:layout_height="90px" />
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index a29811d..f227606 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -506,7 +506,7 @@
     private int mDispatchScrollCounter = 0;
 
     @NonNull
-    private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory();
+    private EdgeEffectFactory mEdgeEffectFactory = sDefaultEdgeEffectFactory;
     private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
 
     ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -614,6 +614,9 @@
         }
     };
 
+    static final StretchEdgeEffectFactory sDefaultEdgeEffectFactory =
+            new StretchEdgeEffectFactory();
+
     // These fields are only used to track whether we need to layout and measure RV children in
     // onLayout.
     //
@@ -716,6 +719,7 @@
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
                 defStyleAttr, 0);
+
         ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.RecyclerView,
                 attrs, a, defStyleAttr, 0);
         String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
@@ -1924,6 +1928,12 @@
         if (canScrollVertical) {
             nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
         }
+
+        // If there is no MotionEvent, treat it as center-aligned edge effect:
+        float verticalDisplacement = motionEvent == null ? getHeight() / 2f : motionEvent.getY();
+        float horizontalDisplacement = motionEvent == null ? getWidth() / 2f : motionEvent.getX();
+        x -= releaseHorizontalGlow(x, verticalDisplacement);
+        y -= releaseVerticalGlow(y, horizontalDisplacement);
         startNestedScroll(nestedScrollAxis, type);
         if (dispatchNestedPreScroll(
                 canScrollHorizontal ? x : 0,
@@ -2108,6 +2118,73 @@
     }
 
     /**
+     * If either of the horizontal edge glows are currently active, this consumes part or all of
+     * deltaX on the edge glow.
+     *
+     * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive
+     *                         for moving down and negative for moving up.
+     * @param y The vertical position of the pointer.
+     * @return The amount of <code>deltaX</code> that has been consumed by the
+     * edge glow.
+     */
+    private int releaseHorizontalGlow(int deltaX, float y) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = y / getHeight();
+        float pullDistance = (float) deltaX / getWidth();
+        if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mLeftGlow, -pullDistance, 1 - displacement);
+            if (EdgeEffectCompat.getDistance(mLeftGlow) == 0) {
+                mLeftGlow.onRelease();
+            }
+        } else if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mRightGlow, pullDistance, displacement);
+            if (EdgeEffectCompat.getDistance(mRightGlow) == 0) {
+                mRightGlow.onRelease();
+            }
+        }
+        int pixelsConsumed = Math.round(consumed * getWidth());
+        if (pixelsConsumed != 0) {
+            invalidate();
+        }
+        return pixelsConsumed;
+    }
+
+    /**
+     * If either of the vertical edge glows are currently active, this consumes part or all of
+     * deltaY on the edge glow.
+     *
+     * @param deltaY The pointer motion, in pixels, in the vertical direction, positive
+     *                         for moving down and negative for moving up.
+     * @param x The vertical position of the pointer.
+     * @return The amount of <code>deltaY</code> that has been consumed by the
+     * edge glow.
+     */
+    private int releaseVerticalGlow(int deltaY, float x) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = x / getWidth();
+        float pullDistance = (float) deltaY / getHeight();
+        if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mTopGlow, -pullDistance, displacement);
+            if (EdgeEffectCompat.getDistance(mTopGlow) == 0) {
+                mTopGlow.onRelease();
+            }
+        } else if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mBottomGlow, pullDistance,
+                    1 - displacement);
+            if (EdgeEffectCompat.getDistance(mBottomGlow) == 0) {
+                mBottomGlow.onRelease();
+            }
+        }
+        int pixelsConsumed = Math.round(consumed * getHeight());
+        if (pixelsConsumed != 0) {
+            invalidate();
+        }
+        return pixelsConsumed;
+    }
+
+    /**
      * <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
      * range. This value is used to compute the length of the thumb within the scrollbar's track.
      * </p>
@@ -2591,6 +2668,35 @@
             return false;
         }
 
+        // Flinging while the edge effect is active should affect the edge effect,
+        // not scrolling.
+        boolean flung = false;
+        if (velocityX != 0) {
+            if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) {
+                mLeftGlow.onAbsorb(-velocityX);
+                velocityX = 0;
+                flung = true;
+            } else if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) {
+                mRightGlow.onAbsorb(velocityX);
+                velocityX = 0;
+                flung = true;
+            }
+        }
+        if (velocityY != 0) {
+            if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) {
+                mTopGlow.onAbsorb(-velocityY);
+                velocityY = 0;
+                flung = true;
+            } else if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) {
+                mBottomGlow.onAbsorb(velocityY);
+                velocityY = 0;
+                flung = true;
+            }
+        }
+        if (velocityX == 0 && velocityY == 0) {
+            return true; // consumed all the velocity in the overscroll fling
+        }
+
         if (!dispatchNestedPreFling(velocityX, velocityY)) {
             final boolean canScroll = canScrollHorizontal || canScrollVertical;
             dispatchNestedFling(velocityX, velocityY, canScroll);
@@ -2615,7 +2721,7 @@
                 return true;
             }
         }
-        return false;
+        return flung;
     }
 
     /**
@@ -2663,21 +2769,23 @@
         boolean invalidate = false;
         if (overscrollX < 0) {
             ensureLeftGlow();
-            EdgeEffectCompat.onPull(mLeftGlow, -overscrollX / getWidth(), 1f - y / getHeight());
+            EdgeEffectCompat.onPullDistance(mLeftGlow, -overscrollX / getWidth(),
+                    1f - y / getHeight());
             invalidate = true;
         } else if (overscrollX > 0) {
             ensureRightGlow();
-            EdgeEffectCompat.onPull(mRightGlow, overscrollX / getWidth(), y / getHeight());
+            EdgeEffectCompat.onPullDistance(mRightGlow, overscrollX / getWidth(), y / getHeight());
             invalidate = true;
         }
 
         if (overscrollY < 0) {
             ensureTopGlow();
-            EdgeEffectCompat.onPull(mTopGlow, -overscrollY / getHeight(), x / getWidth());
+            EdgeEffectCompat.onPullDistance(mTopGlow, -overscrollY / getHeight(), x / getWidth());
             invalidate = true;
         } else if (overscrollY > 0) {
             ensureBottomGlow();
-            EdgeEffectCompat.onPull(mBottomGlow, overscrollY / getHeight(), 1f - x / getWidth());
+            EdgeEffectCompat.onPullDistance(mBottomGlow, overscrollY / getHeight(),
+                    1f - x / getWidth());
             invalidate = true;
         }
 
@@ -2735,26 +2843,18 @@
     void absorbGlows(int velocityX, int velocityY) {
         if (velocityX < 0) {
             ensureLeftGlow();
-            if (mLeftGlow.isFinished()) {
-                mLeftGlow.onAbsorb(-velocityX);
-            }
+            mLeftGlow.onAbsorb(-velocityX);
         } else if (velocityX > 0) {
             ensureRightGlow();
-            if (mRightGlow.isFinished()) {
-                mRightGlow.onAbsorb(velocityX);
-            }
+            mRightGlow.onAbsorb(velocityX);
         }
 
         if (velocityY < 0) {
             ensureTopGlow();
-            if (mTopGlow.isFinished()) {
-                mTopGlow.onAbsorb(-velocityY);
-            }
+            mTopGlow.onAbsorb(-velocityY);
         } else if (velocityY > 0) {
             ensureBottomGlow();
-            if (mBottomGlow.isFinished()) {
-                mBottomGlow.onAbsorb(velocityY);
-            }
+            mBottomGlow.onAbsorb(velocityY);
         }
 
         if (velocityX != 0 || velocityY != 0) {
@@ -3331,7 +3431,7 @@
                 mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                 mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
 
-                if (mScrollState == SCROLL_STATE_SETTLING) {
+                if (stopGlowAnimations(e) || mScrollState == SCROLL_STATE_SETTLING) {
                     getParent().requestDisallowInterceptTouchEvent(true);
                     setScrollState(SCROLL_STATE_DRAGGING);
                     stopNestedScroll(TYPE_NON_TOUCH);
@@ -3403,6 +3503,38 @@
         return mScrollState == SCROLL_STATE_DRAGGING;
     }
 
+    /**
+     * This stops any edge glow animation that is currently running by applying a
+     * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices,
+     * this method does nothing, allowing any animating edge effect to continue animating and
+     * returning <code>false</code> always.
+     *
+     * @param e The motion event to use to indicate the finger position for the displacement of
+     *          the current pull.
+     * @return <code>true</code> if any edge effect had an existing effect to be drawn ond the
+     * animation was stopped or <code>false</code> if no edge effect had a value to display.
+     */
+    private boolean stopGlowAnimations(MotionEvent e) {
+        boolean stopped = false;
+        if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mLeftGlow, 0, 1 - (e.getY() / getHeight()));
+            stopped = true;
+        }
+        if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mRightGlow, 0, e.getY() / getHeight());
+            stopped = true;
+        }
+        if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mTopGlow, 0, e.getX() / getWidth());
+            stopped = true;
+        }
+        if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mBottomGlow, 0, 1 - e.getX() / getWidth());
+            stopped = true;
+        }
+        return stopped;
+    }
+
     @Override
     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
         final int listenerCount = mOnItemTouchListeners.size();
@@ -3511,6 +3643,9 @@
                 if (mScrollState == SCROLL_STATE_DRAGGING) {
                     mReusableIntPair[0] = 0;
                     mReusableIntPair[1] = 0;
+                    dx -= releaseHorizontalGlow(dx, e.getY());
+                    dy -= releaseVerticalGlow(dy, e.getX());
+
                     if (dispatchNestedPreScroll(
                             canScrollHorizontally ? dx : 0,
                             canScrollVertically ? dy : 0,
@@ -5806,6 +5941,17 @@
     }
 
     /**
+     * The default EdgeEffectFactory sets the edge effect type of the EdgeEffect.
+     */
+    static class StretchEdgeEffectFactory extends EdgeEffectFactory {
+        @NonNull
+        @Override
+        protected EdgeEffect createEdgeEffect(@NonNull RecyclerView view, int direction) {
+            return new EdgeEffect(view.getContext());
+        }
+    }
+
+    /**
      * RecycledViewPool lets you share Views between multiple RecyclerViews.
      * <p>
      * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
diff --git a/security/security-identity-credential/build.gradle b/security/security-identity-credential/build.gradle
index 4c3cd86..fcae685b 100644
--- a/security/security-identity-credential/build.gradle
+++ b/security/security-identity-credential/build.gradle
@@ -31,6 +31,7 @@
     implementation(project(":biometric:biometric"))
     implementation("org.bouncycastle:bcprov-jdk15on:1.65")
     implementation("org.bouncycastle:bcpkix-jdk15on:1.56")
+    implementation project(path: ':annotation:annotation')
 
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/security/security-identity-credential/src/androidTest/java/androidx/security/identity/DynamicAuthTest.java b/security/security-identity-credential/src/androidTest/java/androidx/security/identity/DynamicAuthTest.java
index 85a738c..60c8f80d 100644
--- a/security/security-identity-credential/src/androidTest/java/androidx/security/identity/DynamicAuthTest.java
+++ b/security/security-identity-credential/src/androidTest/java/androidx/security/identity/DynamicAuthTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -137,15 +136,13 @@
                 cert.getNotBefore().getTime() + kMilliSecsInOneYear - cert.getNotAfter().getTime();
         assertTrue(-allowDriftMilliSecs <= diffMilliSecs && diffMilliSecs <= allowDriftMilliSecs);
 
-        // The extension is expected only if - and only if - the underlying hardware
+        // The extension must be there if the underlying hardware says it
         // supports updating the credential.
         //
-        byte[] icExtension = cert.getExtensionValue("1.3.6.1.4.1.11129.2.1.26");
         if (store.getCapabilities().isUpdateSupported()) {
+            byte[] icExtension = cert.getExtensionValue("1.3.6.1.4.1.11129.2.1.26");
             assertNotNull(icExtension);
             assertArrayEquals(proofOfProvisioningSha256, Util.getPopSha256FromAuthKeyCert(cert));
-        } else {
-            assertNull(icExtension);
         }
 
         // ... and we're done. Clean up after ourselves.
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java
index 3255079..bb1aa08 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredential.java
@@ -16,8 +16,10 @@
 
 package androidx.security.identity;
 
+import android.icu.util.Calendar;
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -31,6 +33,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
 import java.security.cert.X509Certificate;
+import java.time.Instant;
 import java.util.Collection;
 
 import javax.crypto.BadPaddingException;
@@ -269,4 +272,106 @@
     int[] getAuthenticationDataUsageCount() {
         return mCredential.getAuthenticationDataUsageCount();
     }
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    private static class ApiImplS {
+        @DoNotInline
+        static void callSetAllowUsingExpiredKeys(
+                @NonNull android.security.identity.IdentityCredential credential,
+                boolean allowUsingExpiredKeys) {
+            credential.setAllowUsingExpiredKeys(allowUsingExpiredKeys);
+        }
+
+        @DoNotInline
+        static void callStoreStaticAuthenticationData(
+                @NonNull android.security.identity.IdentityCredential credential,
+                @NonNull X509Certificate authenticationKey,
+                @NonNull Instant expirationDate,
+                @NonNull byte[] staticAuthData)
+                throws android.security.identity.UnknownAuthenticationKeyException {
+            credential.storeStaticAuthenticationData(authenticationKey,
+                    expirationDate,
+                    staticAuthData);
+        }
+
+        @DoNotInline
+        static @NonNull byte[] callProveOwnership(
+                @NonNull android.security.identity.IdentityCredential credential,
+                @NonNull byte[] challenge) {
+            return credential.proveOwnership(challenge);
+        }
+
+        @DoNotInline
+        static @NonNull byte[] callDelete(
+                @NonNull android.security.identity.IdentityCredential credential,
+                @NonNull byte[] challenge) {
+            return credential.delete(challenge);
+        }
+
+        @DoNotInline
+        static @NonNull byte[] callUpdate(
+                @NonNull android.security.identity.IdentityCredential credential,
+                @NonNull android.security.identity.PersonalizationData personalizationData) {
+            return credential.update(personalizationData);
+        }
+    }
+
+    @Override
+    public void setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            ApiImplS.callSetAllowUsingExpiredKeys(mCredential, allowUsingExpiredKeys);
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public void storeStaticAuthenticationData(
+            @NonNull X509Certificate authenticationKey,
+            @NonNull Calendar expirationDate,
+            @NonNull byte[] staticAuthData)
+            throws UnknownAuthenticationKeyException {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            try {
+                Instant expirationDateAsInstant =
+                        Instant.ofEpochMilli(expirationDate.getTimeInMillis());
+                ApiImplS.callStoreStaticAuthenticationData(mCredential,
+                        authenticationKey,
+                        expirationDateAsInstant,
+                        staticAuthData);
+            } catch (android.security.identity.UnknownAuthenticationKeyException e) {
+                throw new UnknownAuthenticationKeyException(e.getMessage(), e);
+            }
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public @NonNull byte[] proveOwnership(@NonNull byte[] challenge)  {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            return ApiImplS.callProveOwnership(mCredential, challenge);
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public @NonNull byte[] delete(@NonNull byte[] challenge)  {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            return ApiImplS.callDelete(mCredential, challenge);
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public @NonNull byte[] update(@NonNull PersonalizationData personalizationData) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            return ApiImplS.callUpdate(mCredential,
+                    HardwareWritableIdentityCredential.convertPDFromJetpack(personalizationData));
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
 }
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredentialStore.java b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredentialStore.java
index 14337da..4446489 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredentialStore.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareIdentityCredentialStore.java
@@ -17,6 +17,7 @@
 package androidx.security.identity;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Build;
 
 import androidx.annotation.NonNull;
@@ -31,14 +32,17 @@
 class HardwareIdentityCredentialStore extends IdentityCredentialStore {
 
     private static final String TAG = "HardwareIdentityCredentialStore";
+    private final Context mContext;
 
     private android.security.identity.IdentityCredentialStore mStore = null;
     private boolean mIsDirectAccess = false;
 
     private HardwareIdentityCredentialStore(
             @NonNull android.security.identity.IdentityCredentialStore store,
+            @NonNull Context context,
             boolean isDirectAccess) {
         mStore = store;
+        mContext = context;
         mIsDirectAccess = isDirectAccess;
     }
 
@@ -46,7 +50,7 @@
         android.security.identity.IdentityCredentialStore store =
                 android.security.identity.IdentityCredentialStore.getInstance(context);
         if (store != null) {
-            return new HardwareIdentityCredentialStore(store, false);
+            return new HardwareIdentityCredentialStore(store, context, false);
         }
         return null;
     }
@@ -65,7 +69,7 @@
         android.security.identity.IdentityCredentialStore store =
                 android.security.identity.IdentityCredentialStore.getDirectAccessInstance(context);
         if (store != null) {
-            return new HardwareIdentityCredentialStore(store, true);
+            return new HardwareIdentityCredentialStore(store, context, true);
         }
         return null;
     }
@@ -144,19 +148,27 @@
     public @NonNull
     IdentityCredentialStoreCapabilities getCapabilities() {
         LinkedHashSet<String> supportedDocTypesSet =
-                new LinkedHashSet<String>(Arrays.asList(mStore.getSupportedDocTypes()));
+                new LinkedHashSet<>(Arrays.asList(mStore.getSupportedDocTypes()));
 
         if (mCapabilities == null) {
-            // TODO: update for Android 12 platform APIs when available.
-            mCapabilities = new SimpleIdentityCredentialStoreCapabilities(
-                    mIsDirectAccess,
-                    IdentityCredentialStoreCapabilities.FEATURE_VERSION_202009,
-                    true,
-                    supportedDocTypesSet,
-                    false,
-                    false,
-                    false,
-                    false);
+            PackageManager pm = mContext.getPackageManager();
+            String featureName = PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE;
+            if (mIsDirectAccess) {
+                featureName = PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS;
+            }
+
+            if (pm.hasSystemFeature(featureName,
+                    IdentityCredentialStoreCapabilities.FEATURE_VERSION_202101)) {
+                mCapabilities = SimpleIdentityCredentialStoreCapabilities.getFeatureVersion202101(
+                        mIsDirectAccess,
+                        true,
+                        supportedDocTypesSet);
+            } else {
+                mCapabilities = SimpleIdentityCredentialStoreCapabilities.getFeatureVersion202009(
+                        mIsDirectAccess,
+                        true,
+                        supportedDocTypesSet);
+            }
         }
         return mCapabilities;
     }
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareWritableIdentityCredential.java b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareWritableIdentityCredential.java
index ab359458..686c500 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareWritableIdentityCredential.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/HardwareWritableIdentityCredential.java
@@ -43,9 +43,8 @@
         return mWritableCredential.getCredentialKeyCertificateChain(challenge);
     }
 
-    @Override
-    @NonNull
-    public byte[] personalize(@NonNull PersonalizationData personalizationData) {
+    static @NonNull android.security.identity.PersonalizationData convertPDFromJetpack(
+            @NonNull PersonalizationData personalizationData) {
 
         android.security.identity.PersonalizationData.Builder builder =
                 new android.security.identity.PersonalizationData.Builder();
@@ -75,6 +74,12 @@
             builder.addAccessControlProfile(profileBuilder.build());
         }
 
-        return mWritableCredential.personalize(builder.build());
+        return builder.build();
+    }
+
+    @Override
+    @NonNull
+    public byte[] personalize(@NonNull PersonalizationData personalizationData) {
+        return mWritableCredential.personalize(convertPDFromJetpack(personalizationData));
     }
 }
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/SimpleIdentityCredentialStoreCapabilities.java b/security/security-identity-credential/src/main/java/androidx/security/identity/SimpleIdentityCredentialStoreCapabilities.java
index 5393bf8..3c35587 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/SimpleIdentityCredentialStoreCapabilities.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/SimpleIdentityCredentialStoreCapabilities.java
@@ -50,6 +50,36 @@
                 isStaticAuthenticationDataExpirationDateSupported;
     }
 
+    static SimpleIdentityCredentialStoreCapabilities getFeatureVersion202009(
+            boolean isDirectAccess,
+            boolean isHardwareBacked,
+            Set<String> supportedDocTypesSet) {
+        return new SimpleIdentityCredentialStoreCapabilities(
+                isDirectAccess,
+                IdentityCredentialStoreCapabilities.FEATURE_VERSION_202009,
+                isHardwareBacked,
+                supportedDocTypesSet,
+                false,
+                false,
+                false,
+                false);
+    }
+
+    static SimpleIdentityCredentialStoreCapabilities getFeatureVersion202101(
+            boolean isDirectAccess,
+            boolean isHardwareBacked,
+            Set<String> supportedDocTypesSet) {
+        return new SimpleIdentityCredentialStoreCapabilities(
+                isDirectAccess,
+                IdentityCredentialStoreCapabilities.FEATURE_VERSION_202101,
+                isHardwareBacked,
+                supportedDocTypesSet,
+                true,
+                true,
+                true,
+                true);
+    }
+
     @Override
     public boolean isDirectAccess() {
         return mIsDirectAccess;
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/SoftwareIdentityCredentialStore.java b/security/security-identity-credential/src/main/java/androidx/security/identity/SoftwareIdentityCredentialStore.java
index 9aff8dc..d5bac8d 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/SoftwareIdentityCredentialStore.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/SoftwareIdentityCredentialStore.java
@@ -69,15 +69,11 @@
     public @NonNull
     IdentityCredentialStoreCapabilities getCapabilities() {
         if (mCapabilities == null) {
-            mCapabilities = new SimpleIdentityCredentialStoreCapabilities(
+            LinkedHashSet<String> supportedDocTypesSet = new LinkedHashSet<>();
+            mCapabilities = SimpleIdentityCredentialStoreCapabilities.getFeatureVersion202101(
                     false,
-                    IdentityCredentialStoreCapabilities.FEATURE_VERSION_202101,
                     false,
-                    new LinkedHashSet<String>(),
-                    true,
-                    true,
-                    true,
-                    true);
+                    supportedDocTypesSet);
         }
         return mCapabilities;
     }
diff --git a/settings.gradle b/settings.gradle
index 71509e0..fec6fa8 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -204,7 +204,12 @@
 includeProject(":appcompat:integration-tests:receive-content-testapp", "appcompat/integration-tests/receive-content-testapp", [BuildType.MAIN])
 includeProject(":appsearch:appsearch", "appsearch/appsearch", [BuildType.MAIN])
 includeProject(":appsearch:appsearch-compiler", "appsearch/compiler", [BuildType.MAIN])
+includeProject(":appsearch:appsearch-debug-view", "appsearch/debug-view", [BuildType.MAIN])
+includeProject(":appsearch:appsearch-debug-view:samples", "appsearch/debug-view/samples",
+        [BuildType.MAIN])
+includeProject(":appsearch:appsearch-ktx", "appsearch/appsearch-ktx", [BuildType.MAIN])
 includeProject(":appsearch:appsearch-local-storage", "appsearch/local-storage", [BuildType.MAIN])
+includeProject(":appsearch:appsearch-platform-storage", "appsearch/platform-storage", [BuildType.MAIN])
 includeProject(":arch:core:core-common", "arch/core/core-common", [BuildType.MAIN])
 includeProject(":arch:core:core-runtime", "arch/core/core-runtime", [BuildType.MAIN])
 includeProject(":arch:core:core-testing", "arch/core/core-testing", [BuildType.MAIN])
@@ -389,6 +394,9 @@
 includeProject(":core:core-appdigest", "core/core-appdigest", [BuildType.MAIN])
 includeProject(":core:core-google-shortcuts", "core/core-google-shortcuts", [BuildType.MAIN])
 includeProject(":core:core-ktx", "core/core-ktx", [BuildType.MAIN])
+includeProject(":core:core-remoteviews", "core/core-remoteviews", [BuildType.MAIN])
+includeProject(":core:core-splashscreen", "core/core-splashscreen", [BuildType.MAIN])
+includeProject(":core:core-splashscreen:core-splashscreen-sample", "core/core-splashscreen/core-splashscreen-sample", [BuildType.MAIN])
 includeProject(":core:core-role", "core/core-role", [BuildType.MAIN])
 includeProject(":cursoradapter:cursoradapter", "cursoradapter/cursoradapter", [BuildType.MAIN])
 includeProject(":customview:customview", "customview/customview", [BuildType.MAIN])
diff --git a/viewpager/viewpager/build.gradle b/viewpager/viewpager/build.gradle
index 0d2e668..40c8c5a 100644
--- a/viewpager/viewpager/build.gradle
+++ b/viewpager/viewpager/build.gradle
@@ -8,7 +8,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    implementation("androidx.core:core:1.3.0-beta01")
+    implementation project(":core:core")
     api("androidx.customview:customview:1.0.0")
 
     androidTestImplementation(libs.testExtJunit)
@@ -18,6 +18,7 @@
     androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation project(':internal-testutils-espresso')
 }
 
 androidx {
diff --git a/viewpager/viewpager/lint-baseline.xml b/viewpager/viewpager/lint-baseline.xml
index 25cc6cd..907f9d0 100644
--- a/viewpager/viewpager/lint-baseline.xml
+++ b/viewpager/viewpager/lint-baseline.xml
@@ -8,7 +8,7 @@
         errorLine2="                                                   ~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="789"
+            line="822"
             column="52"/>
     </issue>
 
@@ -52,7 +52,7 @@
         errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="711"
+            line="744"
             column="41"/>
     </issue>
 
@@ -63,7 +63,7 @@
         errorLine2="                                     ~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="912"
+            line="945"
             column="38"/>
     </issue>
 
@@ -74,7 +74,7 @@
         errorLine2="                                  ~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="1390"
+            line="1423"
             column="35"/>
     </issue>
 
@@ -85,7 +85,7 @@
         errorLine2="           ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="1431"
+            line="1464"
             column="12"/>
     </issue>
 
@@ -96,7 +96,7 @@
         errorLine2="                                       ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="1442"
+            line="1475"
             column="40"/>
     </issue>
 
@@ -107,7 +107,7 @@
         errorLine2="                        ~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="1462"
+            line="1495"
             column="25"/>
     </issue>
 
@@ -118,7 +118,7 @@
         errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="1462"
+            line="1495"
             column="48"/>
     </issue>
 
@@ -129,7 +129,7 @@
         errorLine2="                           ~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="1486"
+            line="1519"
             column="28"/>
     </issue>
 
@@ -140,7 +140,7 @@
         errorLine2="                                         ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2028"
+            line="2061"
             column="42"/>
     </issue>
 
@@ -151,7 +151,7 @@
         errorLine2="                                ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2169"
+            line="2214"
             column="33"/>
     </issue>
 
@@ -162,7 +162,7 @@
         errorLine2="                     ~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2429"
+            line="2507"
             column="22"/>
     </issue>
 
@@ -173,7 +173,7 @@
         errorLine2="                          ~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2471"
+            line="2549"
             column="27"/>
     </issue>
 
@@ -184,7 +184,7 @@
         errorLine2="                                ~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2713"
+            line="2791"
             column="33"/>
     </issue>
 
@@ -195,7 +195,7 @@
         errorLine2="                                    ~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2737"
+            line="2815"
             column="37"/>
     </issue>
 
@@ -206,7 +206,7 @@
         errorLine2="                              ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2899"
+            line="2977"
             column="31"/>
     </issue>
 
@@ -217,7 +217,7 @@
         errorLine2="                              ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2941"
+            line="3019"
             column="31"/>
     </issue>
 
@@ -228,7 +228,7 @@
         errorLine2="            ~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2961"
+            line="3039"
             column="13"/>
     </issue>
 
@@ -239,7 +239,7 @@
         errorLine2="                                                      ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="2990"
+            line="3068"
             column="55"/>
     </issue>
 
@@ -250,7 +250,7 @@
         errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3013"
+            line="3091"
             column="15"/>
     </issue>
 
@@ -261,7 +261,7 @@
         errorLine2="              ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3018"
+            line="3096"
             column="15"/>
     </issue>
 
@@ -272,7 +272,7 @@
         errorLine2="                                                          ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3018"
+            line="3096"
             column="59"/>
     </issue>
 
@@ -283,7 +283,7 @@
         errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3023"
+            line="3101"
             column="41"/>
     </issue>
 
@@ -294,7 +294,7 @@
         errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3028"
+            line="3106"
             column="12"/>
     </issue>
 
@@ -305,7 +305,7 @@
         errorLine2="                                                       ~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3028"
+            line="3106"
             column="56"/>
     </issue>
 
@@ -316,7 +316,7 @@
         errorLine2="                            ~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3143"
+            line="3221"
             column="29"/>
     </issue>
 
@@ -327,7 +327,7 @@
         errorLine2="                                             ~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/viewpager/widget/ViewPager.java"
-            line="3143"
+            line="3221"
             column="46"/>
     </issue>
 
diff --git a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
index 6423be2..52100a6 100644
--- a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
+++ b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
@@ -53,6 +53,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
+import android.content.Context;
 import android.graphics.Color;
 import android.support.v4.testutils.TestUtilsMatchers;
 import android.text.TextUtils;
@@ -61,9 +62,11 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
+import android.widget.EdgeEffect;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.espresso.ViewAction;
 import androidx.test.espresso.action.EspressoKey;
 import androidx.test.filters.FlakyTest;
@@ -94,6 +97,13 @@
     @Rule
     public final ActivityTestRule<T> mActivityTestRule;
 
+    /**
+     * The distance of a swipe's start position from the view's edge, in terms of the view's length.
+     * We do not start the swipe exactly on the view's edge, but somewhat more inward, since swiping
+     * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer).
+     */
+    private static final float EDGE_FUZZ_FACTOR = 0.083f;
+
     private static final int DIRECTION_LEFT = -1;
     private static final int DIRECTION_RIGHT = 1;
     protected ViewPager mViewPager;
@@ -336,7 +346,8 @@
         verifyPageSelections(true);
     }
 
-    private void verifyPageChangeViewActions(ViewAction next, ViewAction previous) {
+    private void verifyPageChangeViewActions(ViewAction next, ViewAction previous)
+            throws Throwable {
         assertEquals("Initial state", 0, mViewPager.getCurrentItem());
         assertFalse(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
         assertTrue(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
@@ -361,6 +372,8 @@
         onView(withId(R.id.pager)).perform(next);
         assertEquals("Attempt to move to next page beyond last page", 2,
                 mViewPager.getCurrentItem());
+
+        waitForEdgeAnimations();
         // We're still on this page, so we shouldn't have been called again with index 2
         verify(mockPageChangeListener, times(1)).onPageSelected(2);
         assertTrue(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
@@ -388,6 +401,7 @@
         verify(mockPageChangeListener, times(1)).onPageSelected(0);
         assertFalse(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
         assertTrue(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
+        waitForEdgeAnimations();
 
         mViewPager.removeOnPageChangeListener(mockPageChangeListener);
 
@@ -397,38 +411,84 @@
         assertThat(pageSelectedCaptor.getAllValues(), TestUtilsMatchers.matches(1, 2, 1, 0));
     }
 
+    private void waitForEdgeAnimations() throws Throwable {
+        while (!mViewPager.mLeftEdge.isFinished() || !mViewPager.mRightEdge.isFinished()) {
+            mActivityTestRule.runOnUiThread(() -> {});
+        }
+    }
+
     @Test
     @LargeTest
-    public void testPageSwipes() {
+    public void testPageSwipes() throws Throwable {
         verifyPageChangeViewActions(ViewPagerActions.wrap(swipeLeft()), ViewPagerActions.wrap(swipeRight()));
     }
 
     @Test
     @LargeTest
-    public void testArrowPageChanges() {
+    public void testArrowPageChanges() throws Throwable {
         verifyPageChangeViewActions(
                 ViewPagerActions.arrowScroll(View.FOCUS_RIGHT), ViewPagerActions.arrowScroll(View.FOCUS_LEFT));
     }
 
     @Test
     @LargeTest
-    public void testPageSwipesComposite() {
+    public void testPageSwipesComposite() throws Throwable {
         assertEquals("Initial state", 0, mViewPager.getCurrentItem());
 
         onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()), ViewPagerActions.wrap(swipeLeft()));
         assertEquals("Swipe twice left", 2, mViewPager.getCurrentItem());
 
-        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()), ViewPagerActions.wrap(swipeRight()));
-        assertEquals("Swipe left beyond last page and then right", 1, mViewPager.getCurrentItem());
+        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()));
+
+        waitForEdgeAnimations();
+
+        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeRight()));
+        assertEquals("Swipe left beyond last page and then right", 1,
+                mViewPager.getCurrentItem());
 
         onView(withId(R.id.pager)).perform(
                 ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(swipeRight()));
         assertEquals("Swipe right and then right beyond first page", 0,
                 mViewPager.getCurrentItem());
 
-        onView(withId(R.id.pager)).perform(
-                ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(swipeLeft()));
-        assertEquals("Swipe right beyond first page and then left", 1, mViewPager.getCurrentItem());
+        waitForEdgeAnimations();
+
+        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeRight()));
+
+        waitForEdgeAnimations();
+
+        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()));
+
+        assertEquals("Swipe right beyond first page and then left", 1,
+                mViewPager.getCurrentItem());
+    }
+
+    @Test
+    @MediumTest
+    public void testFlingAfterStretchAtLeft() {
+        if (BuildCompat.isAtLeastS()) {
+            CaptureOnAbsorb edgeEffect = new CaptureOnAbsorb(mViewPager.getContext());
+            mViewPager.mLeftEdge = edgeEffect;
+            onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeRight()));
+            assertTrue(edgeEffect.pullDistance > 0f);
+            assertTrue(edgeEffect.absorbedVelocity > 0);
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testFlingAfterStretchAtRight() throws Throwable {
+        if (BuildCompat.isAtLeastS()) {
+            CaptureOnAbsorb edgeEffect = new CaptureOnAbsorb(mViewPager.getContext());
+            mViewPager.mRightEdge = edgeEffect;
+            mActivityTestRule.runOnUiThread(() -> {
+                mViewPager.setCurrentItem(2);
+            });
+
+            onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()));
+            assertTrue(edgeEffect.pullDistance > 0f);
+            assertTrue(edgeEffect.absorbedVelocity > 0);
+        }
     }
 
     private void verifyPageContent(boolean smoothScroll) {
@@ -1110,4 +1170,25 @@
         onView(is(adapter.getButton(1, 0))).perform(pressKey(KeyEvent.KEYCODE_DPAD_LEFT));
         assertEquals(0, mViewPager.getCurrentItem());
     }
+
+    private static class CaptureOnAbsorb extends EdgeEffect {
+        public int absorbedVelocity;
+        public float pullDistance;
+
+        CaptureOnAbsorb(Context context) {
+            super(context);
+        }
+
+        @Override
+        public float onPullDistance(float deltaDistance, float displacement) {
+            pullDistance += deltaDistance;
+            return super.onPullDistance(deltaDistance, displacement);
+        }
+
+        @Override
+        public void onAbsorb(int velocity) {
+            absorbedVelocity = velocity;
+            super.onAbsorb(velocity);
+        }
+    }
 }
diff --git a/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml b/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml
new file mode 100644
index 0000000..15f3e79
--- /dev/null
+++ b/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<androidx.viewpager.widget.ViewPager
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pager"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
+
diff --git a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
index 1e1e827..81ee482 100644
--- a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
+++ b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
@@ -49,12 +49,15 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.Insets;
 import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.WindowInsetsCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.widget.EdgeEffectCompat;
 import androidx.customview.view.AbsSavedState;
 
 import java.lang.annotation.ElementType;
@@ -231,8 +234,16 @@
     private boolean mFakeDragging;
     private long mFakeDragBeginTime;
 
-    private EdgeEffect mLeftEdge;
-    private EdgeEffect mRightEdge;
+    /** @hide */
+    @VisibleForTesting
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @NonNull
+    public EdgeEffect mLeftEdge;
+    /** @hide */
+    @VisibleForTesting
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @NonNull
+    public EdgeEffect mRightEdge;
 
     private boolean mFirstLayout = true;
     private boolean mCalledSuper;
@@ -391,19 +402,18 @@
 
     public ViewPager(@NonNull Context context) {
         super(context);
-        initViewPager();
+        initViewPager(context, null);
     }
 
     public ViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
-        initViewPager();
+        initViewPager(context, attrs);
     }
 
-    void initViewPager() {
+    void initViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
         setWillNotDraw(false);
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
         setFocusable(true);
-        final Context context = getContext();
         mScroller = new Scroller(context, sInterpolator);
         final ViewConfiguration configuration = ViewConfiguration.get(context);
         final float density = context.getResources().getDisplayMetrics().density;
@@ -2107,7 +2117,7 @@
                 }
                 if (mIsBeingDragged) {
                     // Scroll to follow the motion event
-                    if (performDrag(x)) {
+                    if (performDrag(x, y)) {
                         ViewCompat.postInvalidateOnAnimation(this);
                     }
                 }
@@ -2135,6 +2145,18 @@
                     mIsBeingDragged = true;
                     requestParentDisallowInterceptTouchEvent(true);
                     setScrollState(SCROLL_STATE_DRAGGING);
+                } else if (EdgeEffectCompat.getDistance(mLeftEdge) != 0
+                        || EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+                    // Caught the edge glow animation
+                    mIsBeingDragged = true;
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                    if (EdgeEffectCompat.getDistance(mLeftEdge) != 0) {
+                        EdgeEffectCompat.onPullDistance(mLeftEdge, 0f,
+                                1 - mLastMotionY / getHeight());
+                    }
+                    if (EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+                        EdgeEffectCompat.onPullDistance(mRightEdge, 0f, mLastMotionY / getHeight());
+                    }
                 } else {
                     completeScroll(false);
                     mIsBeingDragged = false;
@@ -2243,7 +2265,7 @@
                     // Scroll to follow the motion event
                     final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                     final float x = ev.getX(activePointerIndex);
-                    needsInvalidate |= performDrag(x);
+                    needsInvalidate |= performDrag(x, ev.getY(activePointerIndex));
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -2267,6 +2289,13 @@
                     setCurrentItemInternal(nextPage, true, true, initialVelocity);
 
                     needsInvalidate = resetTouch();
+                    if (nextPage == currentPage && needsInvalidate) {
+                        if (!mRightEdge.isFinished()) {
+                            mRightEdge.onAbsorb(-initialVelocity);
+                        } else if (!mLeftEdge.isFinished()) {
+                            mLeftEdge.onAbsorb(initialVelocity);
+                        }
+                    }
                 }
                 break;
             case MotionEvent.ACTION_CANCEL:
@@ -2299,7 +2328,7 @@
         endDrag();
         mLeftEdge.onRelease();
         mRightEdge.onRelease();
-        needsInvalidate = mLeftEdge.isFinished() || mRightEdge.isFinished();
+        needsInvalidate = !mLeftEdge.isFinished() || !mRightEdge.isFinished();
         return needsInvalidate;
     }
 
@@ -2310,11 +2339,42 @@
         }
     }
 
-    private boolean performDrag(float x) {
+    /**
+     * If either of the horizontal edge glows are currently active, this consumes part or all of
+     * deltaX on the edge glow.
+     *
+     * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive
+     *                         for moving down and negative for moving up.
+     * @param y The vertical position of the pointer.
+     * @return The amount of <code>deltaX</code> that has been consumed by the
+     * edge glow.
+     */
+    private float releaseHorizontalGlow(float deltaX, float y) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = y / getHeight();
+        float pullDistance = (float) deltaX / getWidth();
+        if (EdgeEffectCompat.getDistance(mLeftEdge) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mLeftEdge, -pullDistance, 1 - displacement);
+        } else if (EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mRightEdge, pullDistance, displacement);
+        }
+        return consumed * getWidth();
+    }
+
+    private boolean performDrag(float x, float y) {
         boolean needsInvalidate = false;
 
-        final float deltaX = mLastMotionX - x;
+        final float dX = mLastMotionX - x;
         mLastMotionX = x;
+        final float releaseConsumed = releaseHorizontalGlow(dX, y);
+        final float deltaX = dX - releaseConsumed;
+        if (releaseConsumed != 0) {
+            needsInvalidate = true;
+        }
+        if (Math.abs(deltaX) < 0.0001f) { // ignore rounding errors from releaseHorizontalGlow()
+            return needsInvalidate;
+        }
 
         float oldScrollX = getScrollX();
         float scrollX = oldScrollX + deltaX;
@@ -2339,14 +2399,14 @@
         if (scrollX < leftBound) {
             if (leftAbsolute) {
                 float over = leftBound - scrollX;
-                mLeftEdge.onPull(Math.abs(over) / width);
+                EdgeEffectCompat.onPullDistance(mLeftEdge, over / width, 1 - y / getHeight());
                 needsInvalidate = true;
             }
             scrollX = leftBound;
         } else if (scrollX > rightBound) {
             if (rightAbsolute) {
                 float over = scrollX - rightBound;
-                mRightEdge.onPull(Math.abs(over) / width);
+                EdgeEffectCompat.onPullDistance(mRightEdge, over / width, y / getHeight());
                 needsInvalidate = true;
             }
             scrollX = rightBound;
@@ -2407,7 +2467,9 @@
 
     private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
         int targetPage;
-        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity
+                && EdgeEffectCompat.getDistance(mLeftEdge) == 0 // don't fling while stretched
+                && EdgeEffectCompat.getDistance(mRightEdge) == 0) {
             targetPage = velocity > 0 ? currentPage : currentPage + 1;
         } else {
             final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 9ee4ee9..df18201 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -34,6 +34,8 @@
         }
     }
     defaultConfig {
+        // This is necessary because "S" is a pre-release SDK.
+        targetSdkVersion "S"
         javaCompileOptions {
             annotationProcessorOptions {
                 arguments = [
diff --git a/work/integration-tests/testapp/src/main/AndroidManifest.xml b/work/integration-tests/testapp/src/main/AndroidManifest.xml
index 17f5540..c6d608d 100644
--- a/work/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/work/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
         <activity android:name=".sherlockholmes.AnalyzeSherlockHolmesActivity" />
         <activity
             android:name="androidx.work.integration.testapp.MainActivity"
+            android:exported="true"
             android:windowSoftInputMode="stateHidden">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -32,8 +33,9 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".imageprocessing.ImageProcessingActivity" />
-        <activity android:name=".RetryActivity">
+        <activity android:name=".imageprocessing.ImageProcessingActivity"
+            android:exported="false" />
+        <activity android:name=".RetryActivity" android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
index 1bc08db..d131c1d 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
@@ -23,8 +23,10 @@
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.core.app.NotificationCompat
+import androidx.core.os.BuildCompat
 import androidx.work.CoroutineWorker
 import androidx.work.Data
+import androidx.work.ExperimentalExpeditedWork
 import androidx.work.ForegroundInfo
 import androidx.work.WorkerParameters
 import androidx.work.workDataOf
@@ -41,18 +43,28 @@
     override suspend fun doWork(): Result {
         val notificationId = inputData.getInt(InputNotificationId, NotificationId)
         val delayTime = inputData.getLong(InputDelayTime, Delay)
-        // Run in the context of a Foreground service
-        setForeground(getForegroundInfo(notificationId))
         val range = 20
         for (i in 1..range) {
             delay(delayTime)
             progress = workDataOf(Progress to i * (100 / range))
             setProgress(progress)
-            setForeground(getForegroundInfo(notificationId))
+            if (!BuildCompat.isAtLeastS()) {
+                // No need for notifications starting S.
+                notificationManager.notify(
+                    notificationId,
+                    getForegroundInfo(notificationId).notification
+                )
+            }
         }
         return Result.success()
     }
 
+    @ExperimentalExpeditedWork
+    override suspend fun getForegroundInfo(): ForegroundInfo {
+        val notificationId = inputData.getInt(InputNotificationId, NotificationId)
+        return getForegroundInfo(notificationId)
+    }
+
     private fun getForegroundInfo(notificationId: Int): ForegroundInfo {
         val percent = progress.getInt(Progress, 0)
         val id = applicationContext.getString(R.string.channel_id)
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
index f65cae7..6ab514d 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
@@ -44,8 +44,10 @@
 import androidx.work.Constraints;
 import androidx.work.Data;
 import androidx.work.ExistingWorkPolicy;
+import androidx.work.ExperimentalExpeditedWork;
 import androidx.work.NetworkType;
 import androidx.work.OneTimeWorkRequest;
+import androidx.work.OutOfQuotaPolicy;
 import androidx.work.PeriodicWorkRequest;
 import androidx.work.WorkContinuation;
 import androidx.work.WorkInfo;
@@ -64,6 +66,7 @@
 /**
  * Main Activity
  */
+@ExperimentalExpeditedWork
 public class MainActivity extends AppCompatActivity {
 
     private static final String PACKAGE_NAME = "androidx.work.integration.testapp";
@@ -413,6 +416,7 @@
                 OneTimeWorkRequest request =
                         new OneTimeWorkRequest.Builder(ForegroundWorker.class)
                                 .setInputData(inputData)
+                                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                                 .setConstraints(new Constraints.Builder()
                                         .setRequiredNetworkType(NetworkType.CONNECTED).build()
                                 ).build();
diff --git a/work/workmanager-gcm/api/2.6.0-beta02.txt b/work/workmanager-gcm/api/2.6.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/work/workmanager-gcm/api/2.6.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/work/workmanager-gcm/api/public_plus_experimental_2.6.0-beta01.txt b/work/workmanager-gcm/api/public_plus_experimental_2.6.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/work/workmanager-gcm/api/public_plus_experimental_2.6.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/work/workmanager-gcm/api/public_plus_experimental_2.6.0-beta02.txt b/work/workmanager-gcm/api/public_plus_experimental_2.6.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/work/workmanager-gcm/api/public_plus_experimental_2.6.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/work/workmanager-gcm/api/res-2.6.0-beta02.txt b/work/workmanager-gcm/api/res-2.6.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-gcm/api/res-2.6.0-beta02.txt
+++ /dev/null
diff --git a/work/workmanager-gcm/api/restricted_2.6.0-beta01.txt b/work/workmanager-gcm/api/restricted_2.6.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/work/workmanager-gcm/api/restricted_2.6.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/work/workmanager-gcm/api/restricted_2.6.0-beta02.txt b/work/workmanager-gcm/api/restricted_2.6.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/work/workmanager-gcm/api/restricted_2.6.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/work/workmanager-ktx/api/2.6.0-beta01.txt b/work/workmanager-ktx/api/2.6.0-beta01.txt
deleted file mode 100644
index 2c5f419..0000000
--- a/work/workmanager-ktx/api/2.6.0-beta01.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
-    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
-    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
-    method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
-    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
-  }
-
-  public final class DataKt {
-    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
-    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
-  }
-
-  public final class ListenableFutureKt {
-  }
-
-  public final class OneTimeWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
-    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
-  }
-
-  public final class OperationKt {
-    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
-  }
-
-  public final class PeriodicWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
-  }
-
-}
-
diff --git a/work/workmanager-ktx/api/2.6.0-beta02.txt b/work/workmanager-ktx/api/2.6.0-beta02.txt
deleted file mode 100644
index 2c5f419..0000000
--- a/work/workmanager-ktx/api/2.6.0-beta02.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
-    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
-    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
-    method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
-    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
-  }
-
-  public final class DataKt {
-    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
-    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
-  }
-
-  public final class ListenableFutureKt {
-  }
-
-  public final class OneTimeWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
-    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
-  }
-
-  public final class OperationKt {
-    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
-  }
-
-  public final class PeriodicWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
-  }
-
-}
-
diff --git a/work/workmanager-ktx/api/public_plus_experimental_2.6.0-beta01.txt b/work/workmanager-ktx/api/public_plus_experimental_2.6.0-beta01.txt
deleted file mode 100644
index 2c5f419..0000000
--- a/work/workmanager-ktx/api/public_plus_experimental_2.6.0-beta01.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
-    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
-    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
-    method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
-    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
-  }
-
-  public final class DataKt {
-    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
-    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
-  }
-
-  public final class ListenableFutureKt {
-  }
-
-  public final class OneTimeWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
-    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
-  }
-
-  public final class OperationKt {
-    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
-  }
-
-  public final class PeriodicWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
-  }
-
-}
-
diff --git a/work/workmanager-ktx/api/public_plus_experimental_2.6.0-beta02.txt b/work/workmanager-ktx/api/public_plus_experimental_2.6.0-beta02.txt
deleted file mode 100644
index 2c5f419..0000000
--- a/work/workmanager-ktx/api/public_plus_experimental_2.6.0-beta02.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
-    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
-    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
-    method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
-    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
-  }
-
-  public final class DataKt {
-    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
-    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
-  }
-
-  public final class ListenableFutureKt {
-  }
-
-  public final class OneTimeWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
-    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
-  }
-
-  public final class OperationKt {
-    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
-  }
-
-  public final class PeriodicWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
-  }
-
-}
-
diff --git a/work/workmanager-ktx/api/public_plus_experimental_current.txt b/work/workmanager-ktx/api/public_plus_experimental_current.txt
index 2c5f419..2d666e3 100644
--- a/work/workmanager-ktx/api/public_plus_experimental_current.txt
+++ b/work/workmanager-ktx/api/public_plus_experimental_current.txt
@@ -5,6 +5,8 @@
     ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
     method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
     method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method @androidx.work.ExperimentalExpeditedWork public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
+    method @androidx.work.ExperimentalExpeditedWork public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
     method public final void onStopped();
     method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
diff --git a/work/workmanager-ktx/api/res-2.6.0-beta02.txt b/work/workmanager-ktx/api/res-2.6.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-ktx/api/res-2.6.0-beta02.txt
+++ /dev/null
diff --git a/work/workmanager-ktx/api/restricted_2.6.0-beta01.txt b/work/workmanager-ktx/api/restricted_2.6.0-beta01.txt
deleted file mode 100644
index 2c5f419..0000000
--- a/work/workmanager-ktx/api/restricted_2.6.0-beta01.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
-    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
-    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
-    method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
-    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
-  }
-
-  public final class DataKt {
-    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
-    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
-  }
-
-  public final class ListenableFutureKt {
-  }
-
-  public final class OneTimeWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
-    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
-  }
-
-  public final class OperationKt {
-    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
-  }
-
-  public final class PeriodicWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
-  }
-
-}
-
diff --git a/work/workmanager-ktx/api/restricted_2.6.0-beta02.txt b/work/workmanager-ktx/api/restricted_2.6.0-beta02.txt
deleted file mode 100644
index 2c5f419..0000000
--- a/work/workmanager-ktx/api/restricted_2.6.0-beta02.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
-    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
-    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
-    method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
-    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
-  }
-
-  public final class DataKt {
-    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
-    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
-  }
-
-  public final class ListenableFutureKt {
-  }
-
-  public final class OneTimeWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
-    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
-  }
-
-  public final class OperationKt {
-    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS> p);
-  }
-
-  public final class PeriodicWorkRequestKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
-    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
-  }
-
-}
-
diff --git a/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt b/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
index 81d2a56..5f038d0 100644
--- a/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
+++ b/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
@@ -62,7 +62,6 @@
 
     @Suppress("DEPRECATION")
     public final override fun startWork(): ListenableFuture<Result> {
-
         val coroutineScope = CoroutineScope(coroutineContext + job)
         coroutineScope.launch {
             try {
@@ -72,7 +71,6 @@
                 future.setException(t)
             }
         }
-
         return future
     }
 
@@ -93,6 +91,17 @@
     public abstract suspend fun doWork(): Result
 
     /**
+     * @return The [ForegroundInfo] instance if the [WorkRequest] is marked as expedited.
+     *
+     * @throws [IllegalStateException] when not overridden. Override this method when the
+     * corresponding [WorkRequest] is marked expedited.
+     */
+    @ExperimentalExpeditedWork
+    public open suspend fun getForegroundInfo(): ForegroundInfo {
+        throw IllegalStateException("Not implemented")
+    }
+
+    /**
      * Updates the progress for the [CoroutineWorker]. This is a suspending function unlike the
      * [setProgressAsync] API which returns a [ListenableFuture].
      *
@@ -107,14 +116,30 @@
      * is a suspending function unlike the [setForegroundAsync] API which returns a
      * [ListenableFuture].
      *
+     * Calling [setForeground] will throw an [IllegalStateException] if the process is subject to
+     * foreground service restrictions. Consider using  [WorkRequest.Builder.setExpedited]
+     * and [getForegroundInfo] instead.
+     *
      * @param foregroundInfo The [ForegroundInfo]
      */
     public suspend fun setForeground(foregroundInfo: ForegroundInfo) {
         setForegroundAsync(foregroundInfo).await()
     }
 
+    @Suppress("DEPRECATION")
+    @ExperimentalExpeditedWork
+    public final override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+        val job = Job()
+        val scope = CoroutineScope(coroutineContext + job)
+        val jobFuture = JobListenableFuture<ForegroundInfo>(job)
+        scope.launch {
+            jobFuture.complete(getForegroundInfo())
+        }
+        return jobFuture
+    }
+
     public final override fun onStopped() {
         super.onStopped()
         future.cancel(false)
     }
-}
\ No newline at end of file
+}
diff --git a/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt b/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt
index de621e3..6ae7f33 100644
--- a/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt
+++ b/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt
@@ -19,7 +19,9 @@
 package androidx.work
 
 import androidx.annotation.RestrictTo
+import androidx.work.impl.utils.futures.SettableFuture
 import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.suspendCancellableCoroutine
 import java.util.concurrent.CancellationException
 import java.util.concurrent.ExecutionException
@@ -60,3 +62,26 @@
         )
     }
 }
+
+/**
+ * A special [Job] to [ListenableFuture] wrapper.
+ */
+internal class JobListenableFuture<R>(
+    private val job: Job,
+    private val underlying: SettableFuture<R> = SettableFuture.create()
+) : ListenableFuture<R> by underlying {
+
+    public fun complete(result: R) {
+        underlying.set(result)
+    }
+
+    init {
+        job.invokeOnCompletion { throwable: Throwable? ->
+            when (throwable) {
+                null -> require(underlying.isDone)
+                is CancellationException -> underlying.cancel(true)
+                else -> underlying.setException(throwable.cause ?: throwable)
+            }
+        }
+    }
+}
diff --git a/work/workmanager-multiprocess/api/2.6.0-beta01.txt b/work/workmanager-multiprocess/api/2.6.0-beta01.txt
deleted file mode 100644
index e033a49..0000000
--- a/work/workmanager-multiprocess/api/2.6.0-beta01.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-// Signature format: 4.0
-package androidx.work.multiprocess {
-
-  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
-    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
-    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method public final void onStopped();
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
-  }
-
-  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
-    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
-    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
-  }
-
-  public class RemoteWorkerService extends android.app.Service {
-    ctor public RemoteWorkerService();
-    method public android.os.IBinder? onBind(android.content.Intent);
-  }
-
-}
-
diff --git a/work/workmanager-multiprocess/api/2.6.0-beta02.txt b/work/workmanager-multiprocess/api/2.6.0-beta02.txt
deleted file mode 100644
index e033a49..0000000
--- a/work/workmanager-multiprocess/api/2.6.0-beta02.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-// Signature format: 4.0
-package androidx.work.multiprocess {
-
-  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
-    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
-    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method public final void onStopped();
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
-  }
-
-  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
-    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
-    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
-  }
-
-  public class RemoteWorkerService extends android.app.Service {
-    ctor public RemoteWorkerService();
-    method public android.os.IBinder? onBind(android.content.Intent);
-  }
-
-}
-
diff --git a/work/workmanager-multiprocess/api/public_plus_experimental_2.6.0-beta01.txt b/work/workmanager-multiprocess/api/public_plus_experimental_2.6.0-beta01.txt
deleted file mode 100644
index e033a49..0000000
--- a/work/workmanager-multiprocess/api/public_plus_experimental_2.6.0-beta01.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-// Signature format: 4.0
-package androidx.work.multiprocess {
-
-  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
-    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
-    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method public final void onStopped();
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
-  }
-
-  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
-    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
-    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
-  }
-
-  public class RemoteWorkerService extends android.app.Service {
-    ctor public RemoteWorkerService();
-    method public android.os.IBinder? onBind(android.content.Intent);
-  }
-
-}
-
diff --git a/work/workmanager-multiprocess/api/public_plus_experimental_2.6.0-beta02.txt b/work/workmanager-multiprocess/api/public_plus_experimental_2.6.0-beta02.txt
deleted file mode 100644
index e033a49..0000000
--- a/work/workmanager-multiprocess/api/public_plus_experimental_2.6.0-beta02.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-// Signature format: 4.0
-package androidx.work.multiprocess {
-
-  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
-    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
-    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method public final void onStopped();
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
-  }
-
-  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
-    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
-    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
-  }
-
-  public class RemoteWorkerService extends android.app.Service {
-    ctor public RemoteWorkerService();
-    method public android.os.IBinder? onBind(android.content.Intent);
-  }
-
-}
-
diff --git a/work/workmanager-multiprocess/api/res-2.6.0-beta01.txt b/work/workmanager-multiprocess/api/res-2.6.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-multiprocess/api/res-2.6.0-beta01.txt
+++ /dev/null
diff --git a/work/workmanager-multiprocess/api/res-2.6.0-beta02.txt b/work/workmanager-multiprocess/api/res-2.6.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-multiprocess/api/res-2.6.0-beta02.txt
+++ /dev/null
diff --git a/work/workmanager-multiprocess/api/restricted_2.6.0-beta01.txt b/work/workmanager-multiprocess/api/restricted_2.6.0-beta01.txt
deleted file mode 100644
index e033a49..0000000
--- a/work/workmanager-multiprocess/api/restricted_2.6.0-beta01.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-// Signature format: 4.0
-package androidx.work.multiprocess {
-
-  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
-    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
-    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method public final void onStopped();
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
-  }
-
-  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
-    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
-    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
-  }
-
-  public class RemoteWorkerService extends android.app.Service {
-    ctor public RemoteWorkerService();
-    method public android.os.IBinder? onBind(android.content.Intent);
-  }
-
-}
-
diff --git a/work/workmanager-multiprocess/api/restricted_2.6.0-beta02.txt b/work/workmanager-multiprocess/api/restricted_2.6.0-beta02.txt
deleted file mode 100644
index e033a49..0000000
--- a/work/workmanager-multiprocess/api/restricted_2.6.0-beta02.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-// Signature format: 4.0
-package androidx.work.multiprocess {
-
-  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
-    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
-    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
-    method public final void onStopped();
-    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
-  }
-
-  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
-    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
-    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
-  }
-
-  public class RemoteWorkerService extends android.app.Service {
-    ctor public RemoteWorkerService();
-    method public android.os.IBinder? onBind(android.content.Intent);
-  }
-
-}
-
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
index d98d45f..ea7ed12 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
@@ -25,6 +25,7 @@
 import androidx.work.Data
 import androidx.work.NetworkType
 import androidx.work.OneTimeWorkRequest
+import androidx.work.OutOfQuotaPolicy
 import androidx.work.WorkRequest
 import androidx.work.multiprocess.parcelable.ParcelConverters
 import androidx.work.multiprocess.parcelable.ParcelableWorkRequest
@@ -120,6 +121,7 @@
             requests += OneTimeWorkRequest.Builder(TestWorker::class.java)
                 .addTag("Test Worker")
                 .keepResultsForAtLeast(1, TimeUnit.DAYS)
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                 .build()
         }
         assertOn(requests)
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
index e784b12..4923b28 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
@@ -18,8 +18,12 @@
 
 import static androidx.work.impl.model.WorkTypeConverters.backoffPolicyToInt;
 import static androidx.work.impl.model.WorkTypeConverters.intToBackoffPolicy;
+import static androidx.work.impl.model.WorkTypeConverters.intToOutOfQuotaPolicy;
 import static androidx.work.impl.model.WorkTypeConverters.intToState;
+import static androidx.work.impl.model.WorkTypeConverters.outOfQuotaPolicyToInt;
 import static androidx.work.impl.model.WorkTypeConverters.stateToInt;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.readBooleanValue;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.writeBooleanValue;
 
 import android.annotation.SuppressLint;
 import android.os.Parcel;
@@ -89,6 +93,10 @@
         workSpec.minimumRetentionDuration = in.readLong();
         // scheduleRequestedAt
         workSpec.scheduleRequestedAt = in.readLong();
+        // expedited
+        workSpec.expedited = readBooleanValue(in);
+        // fallback
+        workSpec.outOfQuotaPolicy = intToOutOfQuotaPolicy(in.readInt());
         mWorkRequest = new WorkRequestHolder(UUID.fromString(id), workSpec, tagsSet);
     }
 
@@ -149,6 +157,10 @@
         parcel.writeLong(workSpec.minimumRetentionDuration);
         // scheduleRequestedAt
         parcel.writeLong(workSpec.scheduleRequestedAt);
+        // expedited
+        writeBooleanValue(parcel, workSpec.expedited);
+        // fallback
+        parcel.writeInt(outOfQuotaPolicyToInt(workSpec.outOfQuotaPolicy));
     }
 
     @NonNull
diff --git a/work/workmanager-rxjava2/api/2.6.0-beta01.txt b/work/workmanager-rxjava2/api/2.6.0-beta01.txt
deleted file mode 100644
index 2d667ee..0000000
--- a/work/workmanager-rxjava2/api/2.6.0-beta01.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
-    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava2/api/2.6.0-beta02.txt b/work/workmanager-rxjava2/api/2.6.0-beta02.txt
deleted file mode 100644
index 2d667ee..0000000
--- a/work/workmanager-rxjava2/api/2.6.0-beta02.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
-    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava2/api/public_plus_experimental_2.6.0-beta01.txt b/work/workmanager-rxjava2/api/public_plus_experimental_2.6.0-beta01.txt
deleted file mode 100644
index 2d667ee..0000000
--- a/work/workmanager-rxjava2/api/public_plus_experimental_2.6.0-beta01.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
-    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava2/api/public_plus_experimental_2.6.0-beta02.txt b/work/workmanager-rxjava2/api/public_plus_experimental_2.6.0-beta02.txt
deleted file mode 100644
index 2d667ee..0000000
--- a/work/workmanager-rxjava2/api/public_plus_experimental_2.6.0-beta02.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
-    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava2/api/res-2.6.0-beta01.txt b/work/workmanager-rxjava2/api/res-2.6.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-rxjava2/api/res-2.6.0-beta01.txt
+++ /dev/null
diff --git a/work/workmanager-rxjava2/api/res-2.6.0-beta02.txt b/work/workmanager-rxjava2/api/res-2.6.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-rxjava2/api/res-2.6.0-beta02.txt
+++ /dev/null
diff --git a/work/workmanager-rxjava2/api/restricted_2.6.0-beta01.txt b/work/workmanager-rxjava2/api/restricted_2.6.0-beta01.txt
deleted file mode 100644
index 2d667ee..0000000
--- a/work/workmanager-rxjava2/api/restricted_2.6.0-beta01.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
-    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava2/api/restricted_2.6.0-beta02.txt b/work/workmanager-rxjava2/api/restricted_2.6.0-beta02.txt
deleted file mode 100644
index 2d667ee..0000000
--- a/work/workmanager-rxjava2/api/restricted_2.6.0-beta02.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
-    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava3/api/2.6.0-beta01.txt b/work/workmanager-rxjava3/api/2.6.0-beta01.txt
deleted file mode 100644
index 9293340..0000000
--- a/work/workmanager-rxjava3/api/2.6.0-beta01.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-// Signature format: 4.0
-package androidx.work.rxjava3 {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava3/api/2.6.0-beta02.txt b/work/workmanager-rxjava3/api/2.6.0-beta02.txt
deleted file mode 100644
index 9293340..0000000
--- a/work/workmanager-rxjava3/api/2.6.0-beta02.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-// Signature format: 4.0
-package androidx.work.rxjava3 {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava3/api/public_plus_experimental_2.6.0-beta01.txt b/work/workmanager-rxjava3/api/public_plus_experimental_2.6.0-beta01.txt
deleted file mode 100644
index 9293340..0000000
--- a/work/workmanager-rxjava3/api/public_plus_experimental_2.6.0-beta01.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-// Signature format: 4.0
-package androidx.work.rxjava3 {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava3/api/public_plus_experimental_2.6.0-beta02.txt b/work/workmanager-rxjava3/api/public_plus_experimental_2.6.0-beta02.txt
deleted file mode 100644
index 9293340..0000000
--- a/work/workmanager-rxjava3/api/public_plus_experimental_2.6.0-beta02.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-// Signature format: 4.0
-package androidx.work.rxjava3 {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava3/api/res-2.6.0-beta01.txt b/work/workmanager-rxjava3/api/res-2.6.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-rxjava3/api/res-2.6.0-beta01.txt
+++ /dev/null
diff --git a/work/workmanager-rxjava3/api/res-2.6.0-beta02.txt b/work/workmanager-rxjava3/api/res-2.6.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-rxjava3/api/res-2.6.0-beta02.txt
+++ /dev/null
diff --git a/work/workmanager-rxjava3/api/restricted_2.6.0-beta01.txt b/work/workmanager-rxjava3/api/restricted_2.6.0-beta01.txt
deleted file mode 100644
index 9293340..0000000
--- a/work/workmanager-rxjava3/api/restricted_2.6.0-beta01.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-// Signature format: 4.0
-package androidx.work.rxjava3 {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-rxjava3/api/restricted_2.6.0-beta02.txt b/work/workmanager-rxjava3/api/restricted_2.6.0-beta02.txt
deleted file mode 100644
index 9293340..0000000
--- a/work/workmanager-rxjava3/api/restricted_2.6.0-beta02.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-// Signature format: 4.0
-package androidx.work.rxjava3 {
-
-  public abstract class RxWorker extends androidx.work.ListenableWorker {
-    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
-    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
-    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
-    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-}
-
diff --git a/work/workmanager-testing/api/2.6.0-beta01.txt b/work/workmanager-testing/api/2.6.0-beta01.txt
deleted file mode 100644
index f3f3fe2..0000000
--- a/work/workmanager-testing/api/2.6.0-beta01.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-// Signature format: 4.0
-package androidx.work.testing {
-
-  public class SynchronousExecutor implements java.util.concurrent.Executor {
-    ctor public SynchronousExecutor();
-    method public void execute(Runnable);
-  }
-
-  public interface TestDriver {
-    method public void setAllConstraintsMet(java.util.UUID);
-    method public void setInitialDelayMet(java.util.UUID);
-    method public void setPeriodDelayMet(java.util.UUID);
-  }
-
-  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
-    method public W build();
-    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
-    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
-    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public final class TestListenableWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
-    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
-    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
-  }
-
-  public final class TestWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public final class WorkManagerTestInitHelper {
-    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
-    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
-  }
-
-}
-
diff --git a/work/workmanager-testing/api/2.6.0-beta02.txt b/work/workmanager-testing/api/2.6.0-beta02.txt
deleted file mode 100644
index f3f3fe2..0000000
--- a/work/workmanager-testing/api/2.6.0-beta02.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-// Signature format: 4.0
-package androidx.work.testing {
-
-  public class SynchronousExecutor implements java.util.concurrent.Executor {
-    ctor public SynchronousExecutor();
-    method public void execute(Runnable);
-  }
-
-  public interface TestDriver {
-    method public void setAllConstraintsMet(java.util.UUID);
-    method public void setInitialDelayMet(java.util.UUID);
-    method public void setPeriodDelayMet(java.util.UUID);
-  }
-
-  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
-    method public W build();
-    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
-    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
-    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public final class TestListenableWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
-    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
-    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
-  }
-
-  public final class TestWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public final class WorkManagerTestInitHelper {
-    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
-    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
-  }
-
-}
-
diff --git a/work/workmanager-testing/api/public_plus_experimental_2.6.0-beta01.txt b/work/workmanager-testing/api/public_plus_experimental_2.6.0-beta01.txt
deleted file mode 100644
index f3f3fe2..0000000
--- a/work/workmanager-testing/api/public_plus_experimental_2.6.0-beta01.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-// Signature format: 4.0
-package androidx.work.testing {
-
-  public class SynchronousExecutor implements java.util.concurrent.Executor {
-    ctor public SynchronousExecutor();
-    method public void execute(Runnable);
-  }
-
-  public interface TestDriver {
-    method public void setAllConstraintsMet(java.util.UUID);
-    method public void setInitialDelayMet(java.util.UUID);
-    method public void setPeriodDelayMet(java.util.UUID);
-  }
-
-  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
-    method public W build();
-    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
-    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
-    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public final class TestListenableWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
-    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
-    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
-  }
-
-  public final class TestWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public final class WorkManagerTestInitHelper {
-    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
-    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
-  }
-
-}
-
diff --git a/work/workmanager-testing/api/public_plus_experimental_2.6.0-beta02.txt b/work/workmanager-testing/api/public_plus_experimental_2.6.0-beta02.txt
deleted file mode 100644
index f3f3fe2..0000000
--- a/work/workmanager-testing/api/public_plus_experimental_2.6.0-beta02.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-// Signature format: 4.0
-package androidx.work.testing {
-
-  public class SynchronousExecutor implements java.util.concurrent.Executor {
-    ctor public SynchronousExecutor();
-    method public void execute(Runnable);
-  }
-
-  public interface TestDriver {
-    method public void setAllConstraintsMet(java.util.UUID);
-    method public void setInitialDelayMet(java.util.UUID);
-    method public void setPeriodDelayMet(java.util.UUID);
-  }
-
-  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
-    method public W build();
-    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
-    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
-    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public final class TestListenableWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
-    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
-    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
-  }
-
-  public final class TestWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public final class WorkManagerTestInitHelper {
-    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
-    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
-  }
-
-}
-
diff --git a/work/workmanager-testing/api/res-2.6.0-beta01.txt b/work/workmanager-testing/api/res-2.6.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-testing/api/res-2.6.0-beta01.txt
+++ /dev/null
diff --git a/work/workmanager-testing/api/res-2.6.0-beta02.txt b/work/workmanager-testing/api/res-2.6.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager-testing/api/res-2.6.0-beta02.txt
+++ /dev/null
diff --git a/work/workmanager-testing/api/restricted_2.6.0-beta01.txt b/work/workmanager-testing/api/restricted_2.6.0-beta01.txt
deleted file mode 100644
index f3f3fe2..0000000
--- a/work/workmanager-testing/api/restricted_2.6.0-beta01.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-// Signature format: 4.0
-package androidx.work.testing {
-
-  public class SynchronousExecutor implements java.util.concurrent.Executor {
-    ctor public SynchronousExecutor();
-    method public void execute(Runnable);
-  }
-
-  public interface TestDriver {
-    method public void setAllConstraintsMet(java.util.UUID);
-    method public void setInitialDelayMet(java.util.UUID);
-    method public void setPeriodDelayMet(java.util.UUID);
-  }
-
-  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
-    method public W build();
-    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
-    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
-    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public final class TestListenableWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
-    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
-    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
-  }
-
-  public final class TestWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public final class WorkManagerTestInitHelper {
-    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
-    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
-  }
-
-}
-
diff --git a/work/workmanager-testing/api/restricted_2.6.0-beta02.txt b/work/workmanager-testing/api/restricted_2.6.0-beta02.txt
deleted file mode 100644
index f3f3fe2..0000000
--- a/work/workmanager-testing/api/restricted_2.6.0-beta02.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-// Signature format: 4.0
-package androidx.work.testing {
-
-  public class SynchronousExecutor implements java.util.concurrent.Executor {
-    ctor public SynchronousExecutor();
-    method public void execute(Runnable);
-  }
-
-  public interface TestDriver {
-    method public void setAllConstraintsMet(java.util.UUID);
-    method public void setInitialDelayMet(java.util.UUID);
-    method public void setPeriodDelayMet(java.util.UUID);
-  }
-
-  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
-    method public W build();
-    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
-    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
-    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
-    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
-    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public final class TestListenableWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
-    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
-    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
-  }
-
-  public final class TestWorkerBuilderKt {
-    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
-  }
-
-  public final class WorkManagerTestInitHelper {
-    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
-    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context);
-    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
-  }
-
-}
-
diff --git a/work/workmanager/api/2.6.0-beta01.txt b/work/workmanager/api/2.6.0-beta01.txt
deleted file mode 100644
index 54713f5..0000000
--- a/work/workmanager/api/2.6.0-beta01.txt
+++ /dev/null
@@ -1,400 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
-    ctor public ArrayCreatingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public enum BackoffPolicy {
-    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
-    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
-  }
-
-  public final class Configuration {
-    method public String? getDefaultProcessName();
-    method public java.util.concurrent.Executor getExecutor();
-    method public androidx.work.InputMergerFactory getInputMergerFactory();
-    method public int getMaxJobSchedulerId();
-    method public int getMinJobSchedulerId();
-    method public androidx.work.RunnableScheduler getRunnableScheduler();
-    method public java.util.concurrent.Executor getTaskExecutor();
-    method public androidx.work.WorkerFactory getWorkerFactory();
-    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
-  }
-
-  public static final class Configuration.Builder {
-    ctor public Configuration.Builder();
-    method public androidx.work.Configuration build();
-    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
-    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
-    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
-    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
-    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
-    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
-    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public static interface Configuration.Provider {
-    method public androidx.work.Configuration getWorkManagerConfiguration();
-  }
-
-  public final class Constraints {
-    ctor public Constraints(androidx.work.Constraints);
-    method public androidx.work.NetworkType getRequiredNetworkType();
-    method public boolean requiresBatteryNotLow();
-    method public boolean requiresCharging();
-    method @RequiresApi(23) public boolean requiresDeviceIdle();
-    method public boolean requiresStorageNotLow();
-    field public static final androidx.work.Constraints NONE;
-  }
-
-  public static final class Constraints.Builder {
-    ctor public Constraints.Builder();
-    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
-    method public androidx.work.Constraints build();
-    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
-    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
-    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
-    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
-    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
-  }
-
-  public final class Data {
-    ctor public Data(androidx.work.Data);
-    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
-    method public boolean getBoolean(String, boolean);
-    method public boolean[]? getBooleanArray(String);
-    method public byte getByte(String, byte);
-    method public byte[]? getByteArray(String);
-    method public double getDouble(String, double);
-    method public double[]? getDoubleArray(String);
-    method public float getFloat(String, float);
-    method public float[]? getFloatArray(String);
-    method public int getInt(String, int);
-    method public int[]? getIntArray(String);
-    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
-    method public long getLong(String, long);
-    method public long[]? getLongArray(String);
-    method public String? getString(String);
-    method public String![]? getStringArray(String);
-    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
-    method public byte[] toByteArray();
-    field public static final androidx.work.Data EMPTY;
-    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
-  }
-
-  public static final class Data.Builder {
-    ctor public Data.Builder();
-    method public androidx.work.Data build();
-    method public androidx.work.Data.Builder putAll(androidx.work.Data);
-    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
-    method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
-    method public androidx.work.Data.Builder putByte(String, byte);
-    method public androidx.work.Data.Builder putByteArray(String, byte[]);
-    method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
-    method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String, float[]);
-    method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]);
-    method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]);
-    method public androidx.work.Data.Builder putString(String, String?);
-    method public androidx.work.Data.Builder putStringArray(String, String![]);
-  }
-
-  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
-    ctor public DelegatingWorkerFactory();
-    method public final void addFactory(androidx.work.WorkerFactory);
-    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public enum ExistingPeriodicWorkPolicy {
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
-  }
-
-  public enum ExistingWorkPolicy {
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
-    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
-  }
-
-  public final class ForegroundInfo {
-    ctor public ForegroundInfo(int, android.app.Notification);
-    ctor public ForegroundInfo(int, android.app.Notification, int);
-    method public int getForegroundServiceType();
-    method public android.app.Notification getNotification();
-    method public int getNotificationId();
-  }
-
-  public interface ForegroundUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
-  }
-
-  public abstract class InputMerger {
-    ctor public InputMerger();
-    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public abstract class InputMergerFactory {
-    ctor public InputMergerFactory();
-    method public abstract androidx.work.InputMerger? createInputMerger(String);
-  }
-
-  public abstract class ListenableWorker {
-    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public final android.content.Context getApplicationContext();
-    method public final java.util.UUID getId();
-    method public final androidx.work.Data getInputData();
-    method @RequiresApi(28) public final android.net.Network? getNetwork();
-    method @IntRange(from=0) public final int getRunAttemptCount();
-    method public final java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
-    method public final boolean isStopped();
-    method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
-    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract static class ListenableWorker.Result {
-    method public static androidx.work.ListenableWorker.Result failure();
-    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
-    method public abstract androidx.work.Data getOutputData();
-    method public static androidx.work.ListenableWorker.Result retry();
-    method public static androidx.work.ListenableWorker.Result success();
-    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
-  }
-
-  public enum NetworkType {
-    enum_constant public static final androidx.work.NetworkType CONNECTED;
-    enum_constant public static final androidx.work.NetworkType METERED;
-    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
-    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
-    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
-    enum_constant public static final androidx.work.NetworkType UNMETERED;
-  }
-
-  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
-    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
-    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
-  }
-
-  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
-    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
-    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
-  }
-
-  public interface Operation {
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
-    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
-  }
-
-  public abstract static class Operation.State {
-  }
-
-  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
-    ctor public Operation.State.FAILURE(Throwable);
-    method public Throwable getThrowable();
-  }
-
-  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
-  }
-
-  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
-  }
-
-  public final class OverwritingInputMerger extends androidx.work.InputMerger {
-    ctor public OverwritingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
-    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
-    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
-  }
-
-  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
-  }
-
-  public interface ProgressUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
-  }
-
-  public interface RunnableScheduler {
-    method public void cancel(Runnable);
-    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
-  }
-
-  public abstract class WorkContinuation {
-    ctor public WorkContinuation();
-    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
-    method public abstract androidx.work.Operation enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
-    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public final class WorkInfo {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
-  }
-
-  public enum WorkInfo.State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
-    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
-    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
-    enum_constant public static final androidx.work.WorkInfo.State FAILED;
-    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
-    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
-  }
-
-  public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public abstract androidx.work.Operation pruneWork();
-  }
-
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
-    ctor public WorkManagerInitializer();
-    method public androidx.work.WorkManager create(android.content.Context);
-    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
-  }
-
-  public final class WorkQuery {
-    method public java.util.List<java.util.UUID!> getIds();
-    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
-    method public java.util.List<java.lang.String!> getTags();
-    method public java.util.List<java.lang.String!> getUniqueWorkNames();
-  }
-
-  public static final class WorkQuery.Builder {
-    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
-    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery build();
-    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
-    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
-    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
-  }
-
-  public abstract class WorkRequest {
-    method public java.util.UUID getId();
-    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
-    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
-    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
-  }
-
-  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
-    method public final B addTag(String);
-    method public final W build();
-    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
-    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
-    method public final B setConstraints(androidx.work.Constraints);
-    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
-    method public final B setInputData(androidx.work.Data);
-  }
-
-  public abstract class Worker extends androidx.work.ListenableWorker {
-    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
-    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract class WorkerFactory {
-    ctor public WorkerFactory();
-    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public final class WorkerParameters {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getInputData();
-    method @RequiresApi(28) public android.net.Network? getNetwork();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
-  }
-
-}
-
-package androidx.work.multiprocess {
-
-  public abstract class RemoteWorkContinuation {
-    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
-    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public abstract class RemoteWorkManager {
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-  }
-
-}
-
diff --git a/work/workmanager/api/2.6.0-beta02.txt b/work/workmanager/api/2.6.0-beta02.txt
deleted file mode 100644
index 54713f5..0000000
--- a/work/workmanager/api/2.6.0-beta02.txt
+++ /dev/null
@@ -1,400 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
-    ctor public ArrayCreatingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public enum BackoffPolicy {
-    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
-    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
-  }
-
-  public final class Configuration {
-    method public String? getDefaultProcessName();
-    method public java.util.concurrent.Executor getExecutor();
-    method public androidx.work.InputMergerFactory getInputMergerFactory();
-    method public int getMaxJobSchedulerId();
-    method public int getMinJobSchedulerId();
-    method public androidx.work.RunnableScheduler getRunnableScheduler();
-    method public java.util.concurrent.Executor getTaskExecutor();
-    method public androidx.work.WorkerFactory getWorkerFactory();
-    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
-  }
-
-  public static final class Configuration.Builder {
-    ctor public Configuration.Builder();
-    method public androidx.work.Configuration build();
-    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
-    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
-    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
-    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
-    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
-    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
-    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public static interface Configuration.Provider {
-    method public androidx.work.Configuration getWorkManagerConfiguration();
-  }
-
-  public final class Constraints {
-    ctor public Constraints(androidx.work.Constraints);
-    method public androidx.work.NetworkType getRequiredNetworkType();
-    method public boolean requiresBatteryNotLow();
-    method public boolean requiresCharging();
-    method @RequiresApi(23) public boolean requiresDeviceIdle();
-    method public boolean requiresStorageNotLow();
-    field public static final androidx.work.Constraints NONE;
-  }
-
-  public static final class Constraints.Builder {
-    ctor public Constraints.Builder();
-    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
-    method public androidx.work.Constraints build();
-    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
-    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
-    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
-    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
-    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
-  }
-
-  public final class Data {
-    ctor public Data(androidx.work.Data);
-    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
-    method public boolean getBoolean(String, boolean);
-    method public boolean[]? getBooleanArray(String);
-    method public byte getByte(String, byte);
-    method public byte[]? getByteArray(String);
-    method public double getDouble(String, double);
-    method public double[]? getDoubleArray(String);
-    method public float getFloat(String, float);
-    method public float[]? getFloatArray(String);
-    method public int getInt(String, int);
-    method public int[]? getIntArray(String);
-    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
-    method public long getLong(String, long);
-    method public long[]? getLongArray(String);
-    method public String? getString(String);
-    method public String![]? getStringArray(String);
-    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
-    method public byte[] toByteArray();
-    field public static final androidx.work.Data EMPTY;
-    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
-  }
-
-  public static final class Data.Builder {
-    ctor public Data.Builder();
-    method public androidx.work.Data build();
-    method public androidx.work.Data.Builder putAll(androidx.work.Data);
-    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
-    method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
-    method public androidx.work.Data.Builder putByte(String, byte);
-    method public androidx.work.Data.Builder putByteArray(String, byte[]);
-    method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
-    method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String, float[]);
-    method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]);
-    method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]);
-    method public androidx.work.Data.Builder putString(String, String?);
-    method public androidx.work.Data.Builder putStringArray(String, String![]);
-  }
-
-  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
-    ctor public DelegatingWorkerFactory();
-    method public final void addFactory(androidx.work.WorkerFactory);
-    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public enum ExistingPeriodicWorkPolicy {
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
-  }
-
-  public enum ExistingWorkPolicy {
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
-    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
-  }
-
-  public final class ForegroundInfo {
-    ctor public ForegroundInfo(int, android.app.Notification);
-    ctor public ForegroundInfo(int, android.app.Notification, int);
-    method public int getForegroundServiceType();
-    method public android.app.Notification getNotification();
-    method public int getNotificationId();
-  }
-
-  public interface ForegroundUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
-  }
-
-  public abstract class InputMerger {
-    ctor public InputMerger();
-    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public abstract class InputMergerFactory {
-    ctor public InputMergerFactory();
-    method public abstract androidx.work.InputMerger? createInputMerger(String);
-  }
-
-  public abstract class ListenableWorker {
-    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public final android.content.Context getApplicationContext();
-    method public final java.util.UUID getId();
-    method public final androidx.work.Data getInputData();
-    method @RequiresApi(28) public final android.net.Network? getNetwork();
-    method @IntRange(from=0) public final int getRunAttemptCount();
-    method public final java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
-    method public final boolean isStopped();
-    method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
-    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract static class ListenableWorker.Result {
-    method public static androidx.work.ListenableWorker.Result failure();
-    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
-    method public abstract androidx.work.Data getOutputData();
-    method public static androidx.work.ListenableWorker.Result retry();
-    method public static androidx.work.ListenableWorker.Result success();
-    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
-  }
-
-  public enum NetworkType {
-    enum_constant public static final androidx.work.NetworkType CONNECTED;
-    enum_constant public static final androidx.work.NetworkType METERED;
-    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
-    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
-    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
-    enum_constant public static final androidx.work.NetworkType UNMETERED;
-  }
-
-  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
-    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
-    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
-  }
-
-  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
-    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
-    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
-  }
-
-  public interface Operation {
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
-    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
-  }
-
-  public abstract static class Operation.State {
-  }
-
-  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
-    ctor public Operation.State.FAILURE(Throwable);
-    method public Throwable getThrowable();
-  }
-
-  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
-  }
-
-  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
-  }
-
-  public final class OverwritingInputMerger extends androidx.work.InputMerger {
-    ctor public OverwritingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
-    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
-    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
-  }
-
-  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
-  }
-
-  public interface ProgressUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
-  }
-
-  public interface RunnableScheduler {
-    method public void cancel(Runnable);
-    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
-  }
-
-  public abstract class WorkContinuation {
-    ctor public WorkContinuation();
-    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
-    method public abstract androidx.work.Operation enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
-    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public final class WorkInfo {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
-  }
-
-  public enum WorkInfo.State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
-    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
-    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
-    enum_constant public static final androidx.work.WorkInfo.State FAILED;
-    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
-    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
-  }
-
-  public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public abstract androidx.work.Operation pruneWork();
-  }
-
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
-    ctor public WorkManagerInitializer();
-    method public androidx.work.WorkManager create(android.content.Context);
-    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
-  }
-
-  public final class WorkQuery {
-    method public java.util.List<java.util.UUID!> getIds();
-    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
-    method public java.util.List<java.lang.String!> getTags();
-    method public java.util.List<java.lang.String!> getUniqueWorkNames();
-  }
-
-  public static final class WorkQuery.Builder {
-    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
-    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery build();
-    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
-    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
-    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
-  }
-
-  public abstract class WorkRequest {
-    method public java.util.UUID getId();
-    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
-    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
-    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
-  }
-
-  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
-    method public final B addTag(String);
-    method public final W build();
-    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
-    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
-    method public final B setConstraints(androidx.work.Constraints);
-    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
-    method public final B setInputData(androidx.work.Data);
-  }
-
-  public abstract class Worker extends androidx.work.ListenableWorker {
-    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
-    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract class WorkerFactory {
-    ctor public WorkerFactory();
-    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public final class WorkerParameters {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getInputData();
-    method @RequiresApi(28) public android.net.Network? getNetwork();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
-  }
-
-}
-
-package androidx.work.multiprocess {
-
-  public abstract class RemoteWorkContinuation {
-    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
-    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public abstract class RemoteWorkManager {
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-  }
-
-}
-
diff --git a/work/workmanager/api/public_plus_experimental_2.6.0-beta01.txt b/work/workmanager/api/public_plus_experimental_2.6.0-beta01.txt
deleted file mode 100644
index 54713f5..0000000
--- a/work/workmanager/api/public_plus_experimental_2.6.0-beta01.txt
+++ /dev/null
@@ -1,400 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
-    ctor public ArrayCreatingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public enum BackoffPolicy {
-    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
-    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
-  }
-
-  public final class Configuration {
-    method public String? getDefaultProcessName();
-    method public java.util.concurrent.Executor getExecutor();
-    method public androidx.work.InputMergerFactory getInputMergerFactory();
-    method public int getMaxJobSchedulerId();
-    method public int getMinJobSchedulerId();
-    method public androidx.work.RunnableScheduler getRunnableScheduler();
-    method public java.util.concurrent.Executor getTaskExecutor();
-    method public androidx.work.WorkerFactory getWorkerFactory();
-    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
-  }
-
-  public static final class Configuration.Builder {
-    ctor public Configuration.Builder();
-    method public androidx.work.Configuration build();
-    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
-    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
-    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
-    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
-    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
-    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
-    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public static interface Configuration.Provider {
-    method public androidx.work.Configuration getWorkManagerConfiguration();
-  }
-
-  public final class Constraints {
-    ctor public Constraints(androidx.work.Constraints);
-    method public androidx.work.NetworkType getRequiredNetworkType();
-    method public boolean requiresBatteryNotLow();
-    method public boolean requiresCharging();
-    method @RequiresApi(23) public boolean requiresDeviceIdle();
-    method public boolean requiresStorageNotLow();
-    field public static final androidx.work.Constraints NONE;
-  }
-
-  public static final class Constraints.Builder {
-    ctor public Constraints.Builder();
-    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
-    method public androidx.work.Constraints build();
-    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
-    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
-    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
-    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
-    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
-  }
-
-  public final class Data {
-    ctor public Data(androidx.work.Data);
-    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
-    method public boolean getBoolean(String, boolean);
-    method public boolean[]? getBooleanArray(String);
-    method public byte getByte(String, byte);
-    method public byte[]? getByteArray(String);
-    method public double getDouble(String, double);
-    method public double[]? getDoubleArray(String);
-    method public float getFloat(String, float);
-    method public float[]? getFloatArray(String);
-    method public int getInt(String, int);
-    method public int[]? getIntArray(String);
-    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
-    method public long getLong(String, long);
-    method public long[]? getLongArray(String);
-    method public String? getString(String);
-    method public String![]? getStringArray(String);
-    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
-    method public byte[] toByteArray();
-    field public static final androidx.work.Data EMPTY;
-    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
-  }
-
-  public static final class Data.Builder {
-    ctor public Data.Builder();
-    method public androidx.work.Data build();
-    method public androidx.work.Data.Builder putAll(androidx.work.Data);
-    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
-    method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
-    method public androidx.work.Data.Builder putByte(String, byte);
-    method public androidx.work.Data.Builder putByteArray(String, byte[]);
-    method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
-    method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String, float[]);
-    method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]);
-    method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]);
-    method public androidx.work.Data.Builder putString(String, String?);
-    method public androidx.work.Data.Builder putStringArray(String, String![]);
-  }
-
-  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
-    ctor public DelegatingWorkerFactory();
-    method public final void addFactory(androidx.work.WorkerFactory);
-    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public enum ExistingPeriodicWorkPolicy {
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
-  }
-
-  public enum ExistingWorkPolicy {
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
-    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
-  }
-
-  public final class ForegroundInfo {
-    ctor public ForegroundInfo(int, android.app.Notification);
-    ctor public ForegroundInfo(int, android.app.Notification, int);
-    method public int getForegroundServiceType();
-    method public android.app.Notification getNotification();
-    method public int getNotificationId();
-  }
-
-  public interface ForegroundUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
-  }
-
-  public abstract class InputMerger {
-    ctor public InputMerger();
-    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public abstract class InputMergerFactory {
-    ctor public InputMergerFactory();
-    method public abstract androidx.work.InputMerger? createInputMerger(String);
-  }
-
-  public abstract class ListenableWorker {
-    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public final android.content.Context getApplicationContext();
-    method public final java.util.UUID getId();
-    method public final androidx.work.Data getInputData();
-    method @RequiresApi(28) public final android.net.Network? getNetwork();
-    method @IntRange(from=0) public final int getRunAttemptCount();
-    method public final java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
-    method public final boolean isStopped();
-    method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
-    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract static class ListenableWorker.Result {
-    method public static androidx.work.ListenableWorker.Result failure();
-    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
-    method public abstract androidx.work.Data getOutputData();
-    method public static androidx.work.ListenableWorker.Result retry();
-    method public static androidx.work.ListenableWorker.Result success();
-    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
-  }
-
-  public enum NetworkType {
-    enum_constant public static final androidx.work.NetworkType CONNECTED;
-    enum_constant public static final androidx.work.NetworkType METERED;
-    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
-    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
-    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
-    enum_constant public static final androidx.work.NetworkType UNMETERED;
-  }
-
-  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
-    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
-    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
-  }
-
-  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
-    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
-    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
-  }
-
-  public interface Operation {
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
-    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
-  }
-
-  public abstract static class Operation.State {
-  }
-
-  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
-    ctor public Operation.State.FAILURE(Throwable);
-    method public Throwable getThrowable();
-  }
-
-  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
-  }
-
-  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
-  }
-
-  public final class OverwritingInputMerger extends androidx.work.InputMerger {
-    ctor public OverwritingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
-    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
-    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
-  }
-
-  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
-  }
-
-  public interface ProgressUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
-  }
-
-  public interface RunnableScheduler {
-    method public void cancel(Runnable);
-    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
-  }
-
-  public abstract class WorkContinuation {
-    ctor public WorkContinuation();
-    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
-    method public abstract androidx.work.Operation enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
-    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public final class WorkInfo {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
-  }
-
-  public enum WorkInfo.State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
-    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
-    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
-    enum_constant public static final androidx.work.WorkInfo.State FAILED;
-    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
-    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
-  }
-
-  public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public abstract androidx.work.Operation pruneWork();
-  }
-
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
-    ctor public WorkManagerInitializer();
-    method public androidx.work.WorkManager create(android.content.Context);
-    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
-  }
-
-  public final class WorkQuery {
-    method public java.util.List<java.util.UUID!> getIds();
-    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
-    method public java.util.List<java.lang.String!> getTags();
-    method public java.util.List<java.lang.String!> getUniqueWorkNames();
-  }
-
-  public static final class WorkQuery.Builder {
-    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
-    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery build();
-    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
-    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
-    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
-  }
-
-  public abstract class WorkRequest {
-    method public java.util.UUID getId();
-    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
-    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
-    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
-  }
-
-  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
-    method public final B addTag(String);
-    method public final W build();
-    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
-    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
-    method public final B setConstraints(androidx.work.Constraints);
-    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
-    method public final B setInputData(androidx.work.Data);
-  }
-
-  public abstract class Worker extends androidx.work.ListenableWorker {
-    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
-    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract class WorkerFactory {
-    ctor public WorkerFactory();
-    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public final class WorkerParameters {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getInputData();
-    method @RequiresApi(28) public android.net.Network? getNetwork();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
-  }
-
-}
-
-package androidx.work.multiprocess {
-
-  public abstract class RemoteWorkContinuation {
-    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
-    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public abstract class RemoteWorkManager {
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-  }
-
-}
-
diff --git a/work/workmanager/api/public_plus_experimental_2.6.0-beta02.txt b/work/workmanager/api/public_plus_experimental_2.6.0-beta02.txt
deleted file mode 100644
index 54713f5..0000000
--- a/work/workmanager/api/public_plus_experimental_2.6.0-beta02.txt
+++ /dev/null
@@ -1,400 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
-    ctor public ArrayCreatingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public enum BackoffPolicy {
-    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
-    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
-  }
-
-  public final class Configuration {
-    method public String? getDefaultProcessName();
-    method public java.util.concurrent.Executor getExecutor();
-    method public androidx.work.InputMergerFactory getInputMergerFactory();
-    method public int getMaxJobSchedulerId();
-    method public int getMinJobSchedulerId();
-    method public androidx.work.RunnableScheduler getRunnableScheduler();
-    method public java.util.concurrent.Executor getTaskExecutor();
-    method public androidx.work.WorkerFactory getWorkerFactory();
-    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
-  }
-
-  public static final class Configuration.Builder {
-    ctor public Configuration.Builder();
-    method public androidx.work.Configuration build();
-    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
-    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
-    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
-    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
-    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
-    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
-    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public static interface Configuration.Provider {
-    method public androidx.work.Configuration getWorkManagerConfiguration();
-  }
-
-  public final class Constraints {
-    ctor public Constraints(androidx.work.Constraints);
-    method public androidx.work.NetworkType getRequiredNetworkType();
-    method public boolean requiresBatteryNotLow();
-    method public boolean requiresCharging();
-    method @RequiresApi(23) public boolean requiresDeviceIdle();
-    method public boolean requiresStorageNotLow();
-    field public static final androidx.work.Constraints NONE;
-  }
-
-  public static final class Constraints.Builder {
-    ctor public Constraints.Builder();
-    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
-    method public androidx.work.Constraints build();
-    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
-    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
-    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
-    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
-    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
-  }
-
-  public final class Data {
-    ctor public Data(androidx.work.Data);
-    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
-    method public boolean getBoolean(String, boolean);
-    method public boolean[]? getBooleanArray(String);
-    method public byte getByte(String, byte);
-    method public byte[]? getByteArray(String);
-    method public double getDouble(String, double);
-    method public double[]? getDoubleArray(String);
-    method public float getFloat(String, float);
-    method public float[]? getFloatArray(String);
-    method public int getInt(String, int);
-    method public int[]? getIntArray(String);
-    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
-    method public long getLong(String, long);
-    method public long[]? getLongArray(String);
-    method public String? getString(String);
-    method public String![]? getStringArray(String);
-    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
-    method public byte[] toByteArray();
-    field public static final androidx.work.Data EMPTY;
-    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
-  }
-
-  public static final class Data.Builder {
-    ctor public Data.Builder();
-    method public androidx.work.Data build();
-    method public androidx.work.Data.Builder putAll(androidx.work.Data);
-    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
-    method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
-    method public androidx.work.Data.Builder putByte(String, byte);
-    method public androidx.work.Data.Builder putByteArray(String, byte[]);
-    method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
-    method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String, float[]);
-    method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]);
-    method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]);
-    method public androidx.work.Data.Builder putString(String, String?);
-    method public androidx.work.Data.Builder putStringArray(String, String![]);
-  }
-
-  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
-    ctor public DelegatingWorkerFactory();
-    method public final void addFactory(androidx.work.WorkerFactory);
-    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public enum ExistingPeriodicWorkPolicy {
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
-  }
-
-  public enum ExistingWorkPolicy {
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
-    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
-  }
-
-  public final class ForegroundInfo {
-    ctor public ForegroundInfo(int, android.app.Notification);
-    ctor public ForegroundInfo(int, android.app.Notification, int);
-    method public int getForegroundServiceType();
-    method public android.app.Notification getNotification();
-    method public int getNotificationId();
-  }
-
-  public interface ForegroundUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
-  }
-
-  public abstract class InputMerger {
-    ctor public InputMerger();
-    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public abstract class InputMergerFactory {
-    ctor public InputMergerFactory();
-    method public abstract androidx.work.InputMerger? createInputMerger(String);
-  }
-
-  public abstract class ListenableWorker {
-    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public final android.content.Context getApplicationContext();
-    method public final java.util.UUID getId();
-    method public final androidx.work.Data getInputData();
-    method @RequiresApi(28) public final android.net.Network? getNetwork();
-    method @IntRange(from=0) public final int getRunAttemptCount();
-    method public final java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
-    method public final boolean isStopped();
-    method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
-    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract static class ListenableWorker.Result {
-    method public static androidx.work.ListenableWorker.Result failure();
-    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
-    method public abstract androidx.work.Data getOutputData();
-    method public static androidx.work.ListenableWorker.Result retry();
-    method public static androidx.work.ListenableWorker.Result success();
-    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
-  }
-
-  public enum NetworkType {
-    enum_constant public static final androidx.work.NetworkType CONNECTED;
-    enum_constant public static final androidx.work.NetworkType METERED;
-    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
-    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
-    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
-    enum_constant public static final androidx.work.NetworkType UNMETERED;
-  }
-
-  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
-    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
-    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
-  }
-
-  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
-    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
-    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
-  }
-
-  public interface Operation {
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
-    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
-  }
-
-  public abstract static class Operation.State {
-  }
-
-  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
-    ctor public Operation.State.FAILURE(Throwable);
-    method public Throwable getThrowable();
-  }
-
-  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
-  }
-
-  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
-  }
-
-  public final class OverwritingInputMerger extends androidx.work.InputMerger {
-    ctor public OverwritingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
-    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
-    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
-  }
-
-  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
-  }
-
-  public interface ProgressUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
-  }
-
-  public interface RunnableScheduler {
-    method public void cancel(Runnable);
-    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
-  }
-
-  public abstract class WorkContinuation {
-    ctor public WorkContinuation();
-    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
-    method public abstract androidx.work.Operation enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
-    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public final class WorkInfo {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
-  }
-
-  public enum WorkInfo.State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
-    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
-    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
-    enum_constant public static final androidx.work.WorkInfo.State FAILED;
-    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
-    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
-  }
-
-  public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public abstract androidx.work.Operation pruneWork();
-  }
-
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
-    ctor public WorkManagerInitializer();
-    method public androidx.work.WorkManager create(android.content.Context);
-    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
-  }
-
-  public final class WorkQuery {
-    method public java.util.List<java.util.UUID!> getIds();
-    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
-    method public java.util.List<java.lang.String!> getTags();
-    method public java.util.List<java.lang.String!> getUniqueWorkNames();
-  }
-
-  public static final class WorkQuery.Builder {
-    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
-    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery build();
-    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
-    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
-    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
-  }
-
-  public abstract class WorkRequest {
-    method public java.util.UUID getId();
-    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
-    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
-    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
-  }
-
-  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
-    method public final B addTag(String);
-    method public final W build();
-    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
-    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
-    method public final B setConstraints(androidx.work.Constraints);
-    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
-    method public final B setInputData(androidx.work.Data);
-  }
-
-  public abstract class Worker extends androidx.work.ListenableWorker {
-    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
-    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract class WorkerFactory {
-    ctor public WorkerFactory();
-    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public final class WorkerParameters {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getInputData();
-    method @RequiresApi(28) public android.net.Network? getNetwork();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
-  }
-
-}
-
-package androidx.work.multiprocess {
-
-  public abstract class RemoteWorkContinuation {
-    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
-    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public abstract class RemoteWorkManager {
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-  }
-
-}
-
diff --git a/work/workmanager/api/public_plus_experimental_current.txt b/work/workmanager/api/public_plus_experimental_current.txt
index 54713f5..0c2f419 100644
--- a/work/workmanager/api/public_plus_experimental_current.txt
+++ b/work/workmanager/api/public_plus_experimental_current.txt
@@ -129,6 +129,9 @@
     enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
   }
 
+  @experimental.Experimental(level=androidx.annotation.experimental.Experimental.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PACKAGE}) public @interface ExperimentalExpeditedWork {
+  }
+
   public final class ForegroundInfo {
     ctor public ForegroundInfo(int, android.app.Notification);
     ctor public ForegroundInfo(int, android.app.Notification, int);
@@ -154,6 +157,7 @@
   public abstract class ListenableWorker {
     ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public final android.content.Context getApplicationContext();
+    method @androidx.work.ExperimentalExpeditedWork public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
     method public final java.util.UUID getId();
     method public final androidx.work.Data getInputData();
     method @RequiresApi(28) public final android.net.Network? getNetwork();
@@ -215,6 +219,11 @@
   public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
   }
 
+  @androidx.work.ExperimentalExpeditedWork public enum OutOfQuotaPolicy {
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
   public final class OverwritingInputMerger extends androidx.work.InputMerger {
     ctor public OverwritingInputMerger();
     method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
@@ -341,6 +350,7 @@
     method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
     method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
     method public final B setConstraints(androidx.work.Constraints);
+    method @androidx.work.ExperimentalExpeditedWork public B setExpedited(androidx.work.OutOfQuotaPolicy);
     method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
     method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
     method public final B setInputData(androidx.work.Data);
diff --git a/work/workmanager/api/res-2.6.0-beta01.txt b/work/workmanager/api/res-2.6.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager/api/res-2.6.0-beta01.txt
+++ /dev/null
diff --git a/work/workmanager/api/res-2.6.0-beta02.txt b/work/workmanager/api/res-2.6.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/work/workmanager/api/res-2.6.0-beta02.txt
+++ /dev/null
diff --git a/work/workmanager/api/restricted_2.6.0-beta01.txt b/work/workmanager/api/restricted_2.6.0-beta01.txt
deleted file mode 100644
index 54713f5..0000000
--- a/work/workmanager/api/restricted_2.6.0-beta01.txt
+++ /dev/null
@@ -1,400 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
-    ctor public ArrayCreatingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public enum BackoffPolicy {
-    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
-    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
-  }
-
-  public final class Configuration {
-    method public String? getDefaultProcessName();
-    method public java.util.concurrent.Executor getExecutor();
-    method public androidx.work.InputMergerFactory getInputMergerFactory();
-    method public int getMaxJobSchedulerId();
-    method public int getMinJobSchedulerId();
-    method public androidx.work.RunnableScheduler getRunnableScheduler();
-    method public java.util.concurrent.Executor getTaskExecutor();
-    method public androidx.work.WorkerFactory getWorkerFactory();
-    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
-  }
-
-  public static final class Configuration.Builder {
-    ctor public Configuration.Builder();
-    method public androidx.work.Configuration build();
-    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
-    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
-    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
-    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
-    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
-    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
-    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public static interface Configuration.Provider {
-    method public androidx.work.Configuration getWorkManagerConfiguration();
-  }
-
-  public final class Constraints {
-    ctor public Constraints(androidx.work.Constraints);
-    method public androidx.work.NetworkType getRequiredNetworkType();
-    method public boolean requiresBatteryNotLow();
-    method public boolean requiresCharging();
-    method @RequiresApi(23) public boolean requiresDeviceIdle();
-    method public boolean requiresStorageNotLow();
-    field public static final androidx.work.Constraints NONE;
-  }
-
-  public static final class Constraints.Builder {
-    ctor public Constraints.Builder();
-    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
-    method public androidx.work.Constraints build();
-    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
-    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
-    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
-    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
-    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
-  }
-
-  public final class Data {
-    ctor public Data(androidx.work.Data);
-    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
-    method public boolean getBoolean(String, boolean);
-    method public boolean[]? getBooleanArray(String);
-    method public byte getByte(String, byte);
-    method public byte[]? getByteArray(String);
-    method public double getDouble(String, double);
-    method public double[]? getDoubleArray(String);
-    method public float getFloat(String, float);
-    method public float[]? getFloatArray(String);
-    method public int getInt(String, int);
-    method public int[]? getIntArray(String);
-    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
-    method public long getLong(String, long);
-    method public long[]? getLongArray(String);
-    method public String? getString(String);
-    method public String![]? getStringArray(String);
-    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
-    method public byte[] toByteArray();
-    field public static final androidx.work.Data EMPTY;
-    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
-  }
-
-  public static final class Data.Builder {
-    ctor public Data.Builder();
-    method public androidx.work.Data build();
-    method public androidx.work.Data.Builder putAll(androidx.work.Data);
-    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
-    method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
-    method public androidx.work.Data.Builder putByte(String, byte);
-    method public androidx.work.Data.Builder putByteArray(String, byte[]);
-    method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
-    method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String, float[]);
-    method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]);
-    method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]);
-    method public androidx.work.Data.Builder putString(String, String?);
-    method public androidx.work.Data.Builder putStringArray(String, String![]);
-  }
-
-  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
-    ctor public DelegatingWorkerFactory();
-    method public final void addFactory(androidx.work.WorkerFactory);
-    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public enum ExistingPeriodicWorkPolicy {
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
-  }
-
-  public enum ExistingWorkPolicy {
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
-    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
-  }
-
-  public final class ForegroundInfo {
-    ctor public ForegroundInfo(int, android.app.Notification);
-    ctor public ForegroundInfo(int, android.app.Notification, int);
-    method public int getForegroundServiceType();
-    method public android.app.Notification getNotification();
-    method public int getNotificationId();
-  }
-
-  public interface ForegroundUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
-  }
-
-  public abstract class InputMerger {
-    ctor public InputMerger();
-    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public abstract class InputMergerFactory {
-    ctor public InputMergerFactory();
-    method public abstract androidx.work.InputMerger? createInputMerger(String);
-  }
-
-  public abstract class ListenableWorker {
-    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public final android.content.Context getApplicationContext();
-    method public final java.util.UUID getId();
-    method public final androidx.work.Data getInputData();
-    method @RequiresApi(28) public final android.net.Network? getNetwork();
-    method @IntRange(from=0) public final int getRunAttemptCount();
-    method public final java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
-    method public final boolean isStopped();
-    method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
-    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract static class ListenableWorker.Result {
-    method public static androidx.work.ListenableWorker.Result failure();
-    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
-    method public abstract androidx.work.Data getOutputData();
-    method public static androidx.work.ListenableWorker.Result retry();
-    method public static androidx.work.ListenableWorker.Result success();
-    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
-  }
-
-  public enum NetworkType {
-    enum_constant public static final androidx.work.NetworkType CONNECTED;
-    enum_constant public static final androidx.work.NetworkType METERED;
-    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
-    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
-    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
-    enum_constant public static final androidx.work.NetworkType UNMETERED;
-  }
-
-  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
-    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
-    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
-  }
-
-  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
-    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
-    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
-  }
-
-  public interface Operation {
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
-    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
-  }
-
-  public abstract static class Operation.State {
-  }
-
-  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
-    ctor public Operation.State.FAILURE(Throwable);
-    method public Throwable getThrowable();
-  }
-
-  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
-  }
-
-  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
-  }
-
-  public final class OverwritingInputMerger extends androidx.work.InputMerger {
-    ctor public OverwritingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
-    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
-    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
-  }
-
-  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
-  }
-
-  public interface ProgressUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
-  }
-
-  public interface RunnableScheduler {
-    method public void cancel(Runnable);
-    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
-  }
-
-  public abstract class WorkContinuation {
-    ctor public WorkContinuation();
-    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
-    method public abstract androidx.work.Operation enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
-    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public final class WorkInfo {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
-  }
-
-  public enum WorkInfo.State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
-    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
-    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
-    enum_constant public static final androidx.work.WorkInfo.State FAILED;
-    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
-    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
-  }
-
-  public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public abstract androidx.work.Operation pruneWork();
-  }
-
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
-    ctor public WorkManagerInitializer();
-    method public androidx.work.WorkManager create(android.content.Context);
-    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
-  }
-
-  public final class WorkQuery {
-    method public java.util.List<java.util.UUID!> getIds();
-    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
-    method public java.util.List<java.lang.String!> getTags();
-    method public java.util.List<java.lang.String!> getUniqueWorkNames();
-  }
-
-  public static final class WorkQuery.Builder {
-    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
-    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery build();
-    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
-    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
-    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
-  }
-
-  public abstract class WorkRequest {
-    method public java.util.UUID getId();
-    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
-    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
-    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
-  }
-
-  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
-    method public final B addTag(String);
-    method public final W build();
-    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
-    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
-    method public final B setConstraints(androidx.work.Constraints);
-    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
-    method public final B setInputData(androidx.work.Data);
-  }
-
-  public abstract class Worker extends androidx.work.ListenableWorker {
-    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
-    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract class WorkerFactory {
-    ctor public WorkerFactory();
-    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public final class WorkerParameters {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getInputData();
-    method @RequiresApi(28) public android.net.Network? getNetwork();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
-  }
-
-}
-
-package androidx.work.multiprocess {
-
-  public abstract class RemoteWorkContinuation {
-    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
-    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public abstract class RemoteWorkManager {
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-  }
-
-}
-
diff --git a/work/workmanager/api/restricted_2.6.0-beta02.txt b/work/workmanager/api/restricted_2.6.0-beta02.txt
deleted file mode 100644
index 54713f5..0000000
--- a/work/workmanager/api/restricted_2.6.0-beta02.txt
+++ /dev/null
@@ -1,400 +0,0 @@
-// Signature format: 4.0
-package androidx.work {
-
-  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
-    ctor public ArrayCreatingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public enum BackoffPolicy {
-    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
-    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
-  }
-
-  public final class Configuration {
-    method public String? getDefaultProcessName();
-    method public java.util.concurrent.Executor getExecutor();
-    method public androidx.work.InputMergerFactory getInputMergerFactory();
-    method public int getMaxJobSchedulerId();
-    method public int getMinJobSchedulerId();
-    method public androidx.work.RunnableScheduler getRunnableScheduler();
-    method public java.util.concurrent.Executor getTaskExecutor();
-    method public androidx.work.WorkerFactory getWorkerFactory();
-    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
-  }
-
-  public static final class Configuration.Builder {
-    ctor public Configuration.Builder();
-    method public androidx.work.Configuration build();
-    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
-    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
-    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
-    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
-    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
-    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
-    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
-    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
-  }
-
-  public static interface Configuration.Provider {
-    method public androidx.work.Configuration getWorkManagerConfiguration();
-  }
-
-  public final class Constraints {
-    ctor public Constraints(androidx.work.Constraints);
-    method public androidx.work.NetworkType getRequiredNetworkType();
-    method public boolean requiresBatteryNotLow();
-    method public boolean requiresCharging();
-    method @RequiresApi(23) public boolean requiresDeviceIdle();
-    method public boolean requiresStorageNotLow();
-    field public static final androidx.work.Constraints NONE;
-  }
-
-  public static final class Constraints.Builder {
-    ctor public Constraints.Builder();
-    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri, boolean);
-    method public androidx.work.Constraints build();
-    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType);
-    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean);
-    method public androidx.work.Constraints.Builder setRequiresCharging(boolean);
-    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean);
-    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration!);
-    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration!);
-  }
-
-  public final class Data {
-    ctor public Data(androidx.work.Data);
-    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
-    method public boolean getBoolean(String, boolean);
-    method public boolean[]? getBooleanArray(String);
-    method public byte getByte(String, byte);
-    method public byte[]? getByteArray(String);
-    method public double getDouble(String, double);
-    method public double[]? getDoubleArray(String);
-    method public float getFloat(String, float);
-    method public float[]? getFloatArray(String);
-    method public int getInt(String, int);
-    method public int[]? getIntArray(String);
-    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
-    method public long getLong(String, long);
-    method public long[]? getLongArray(String);
-    method public String? getString(String);
-    method public String![]? getStringArray(String);
-    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
-    method public byte[] toByteArray();
-    field public static final androidx.work.Data EMPTY;
-    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
-  }
-
-  public static final class Data.Builder {
-    ctor public Data.Builder();
-    method public androidx.work.Data build();
-    method public androidx.work.Data.Builder putAll(androidx.work.Data);
-    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
-    method public androidx.work.Data.Builder putBoolean(String, boolean);
-    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
-    method public androidx.work.Data.Builder putByte(String, byte);
-    method public androidx.work.Data.Builder putByteArray(String, byte[]);
-    method public androidx.work.Data.Builder putDouble(String, double);
-    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
-    method public androidx.work.Data.Builder putFloat(String, float);
-    method public androidx.work.Data.Builder putFloatArray(String, float[]);
-    method public androidx.work.Data.Builder putInt(String, int);
-    method public androidx.work.Data.Builder putIntArray(String, int[]);
-    method public androidx.work.Data.Builder putLong(String, long);
-    method public androidx.work.Data.Builder putLongArray(String, long[]);
-    method public androidx.work.Data.Builder putString(String, String?);
-    method public androidx.work.Data.Builder putStringArray(String, String![]);
-  }
-
-  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
-    ctor public DelegatingWorkerFactory();
-    method public final void addFactory(androidx.work.WorkerFactory);
-    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public enum ExistingPeriodicWorkPolicy {
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
-  }
-
-  public enum ExistingWorkPolicy {
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
-    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
-    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
-    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
-  }
-
-  public final class ForegroundInfo {
-    ctor public ForegroundInfo(int, android.app.Notification);
-    ctor public ForegroundInfo(int, android.app.Notification, int);
-    method public int getForegroundServiceType();
-    method public android.app.Notification getNotification();
-    method public int getNotificationId();
-  }
-
-  public interface ForegroundUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
-  }
-
-  public abstract class InputMerger {
-    ctor public InputMerger();
-    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public abstract class InputMergerFactory {
-    ctor public InputMergerFactory();
-    method public abstract androidx.work.InputMerger? createInputMerger(String);
-  }
-
-  public abstract class ListenableWorker {
-    ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
-    method public final android.content.Context getApplicationContext();
-    method public final java.util.UUID getId();
-    method public final androidx.work.Data getInputData();
-    method @RequiresApi(28) public final android.net.Network? getNetwork();
-    method @IntRange(from=0) public final int getRunAttemptCount();
-    method public final java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
-    method public final boolean isStopped();
-    method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
-    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract static class ListenableWorker.Result {
-    method public static androidx.work.ListenableWorker.Result failure();
-    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
-    method public abstract androidx.work.Data getOutputData();
-    method public static androidx.work.ListenableWorker.Result retry();
-    method public static androidx.work.ListenableWorker.Result success();
-    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
-  }
-
-  public enum NetworkType {
-    enum_constant public static final androidx.work.NetworkType CONNECTED;
-    enum_constant public static final androidx.work.NetworkType METERED;
-    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
-    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
-    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
-    enum_constant public static final androidx.work.NetworkType UNMETERED;
-  }
-
-  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
-    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker>);
-    method public static java.util.List<androidx.work.OneTimeWorkRequest!> from(java.util.List<java.lang.Class<? extends androidx.work.ListenableWorker>!>);
-  }
-
-  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
-    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>);
-    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger>);
-  }
-
-  public interface Operation {
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
-    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
-  }
-
-  public abstract static class Operation.State {
-  }
-
-  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
-    ctor public Operation.State.FAILURE(Throwable);
-    method public Throwable getThrowable();
-  }
-
-  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
-  }
-
-  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
-  }
-
-  public final class OverwritingInputMerger extends androidx.work.InputMerger {
-    ctor public OverwritingInputMerger();
-    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
-  }
-
-  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
-    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
-    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
-  }
-
-  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration);
-    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit);
-    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker>, java.time.Duration, java.time.Duration);
-  }
-
-  public interface ProgressUpdater {
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
-  }
-
-  public interface RunnableScheduler {
-    method public void cancel(Runnable);
-    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
-  }
-
-  public abstract class WorkContinuation {
-    ctor public WorkContinuation();
-    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
-    method public abstract androidx.work.Operation enqueue();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
-    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public final class WorkInfo {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getOutputData();
-    method public androidx.work.Data getProgress();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public androidx.work.WorkInfo.State getState();
-    method public java.util.Set<java.lang.String!> getTags();
-  }
-
-  public enum WorkInfo.State {
-    method public boolean isFinished();
-    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
-    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
-    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
-    enum_constant public static final androidx.work.WorkInfo.State FAILED;
-    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
-    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
-  }
-
-  public abstract class WorkManager {
-    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract androidx.work.Operation cancelAllWork();
-    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
-    method public abstract androidx.work.Operation cancelUniqueWork(String);
-    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
-    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
-    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
-    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
-    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method @Deprecated public static androidx.work.WorkManager getInstance();
-    method public static androidx.work.WorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
-    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
-    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
-    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
-    method public static void initialize(android.content.Context, androidx.work.Configuration);
-    method public abstract androidx.work.Operation pruneWork();
-  }
-
-  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
-    ctor public WorkManagerInitializer();
-    method public androidx.work.WorkManager create(android.content.Context);
-    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
-  }
-
-  public final class WorkQuery {
-    method public java.util.List<java.util.UUID!> getIds();
-    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
-    method public java.util.List<java.lang.String!> getTags();
-    method public java.util.List<java.lang.String!> getUniqueWorkNames();
-  }
-
-  public static final class WorkQuery.Builder {
-    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
-    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
-    method public androidx.work.WorkQuery build();
-    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
-    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
-    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
-    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
-  }
-
-  public abstract class WorkRequest {
-    method public java.util.UUID getId();
-    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
-    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
-    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
-  }
-
-  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<?, ?>, W extends androidx.work.WorkRequest> {
-    method public final B addTag(String);
-    method public final W build();
-    method public final B keepResultsForAtLeast(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration);
-    method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
-    method public final B setConstraints(androidx.work.Constraints);
-    method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
-    method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
-    method public final B setInputData(androidx.work.Data);
-  }
-
-  public abstract class Worker extends androidx.work.ListenableWorker {
-    ctor @Keep public Worker(android.content.Context, androidx.work.WorkerParameters);
-    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
-  }
-
-  public abstract class WorkerFactory {
-    ctor public WorkerFactory();
-    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
-  }
-
-  public final class WorkerParameters {
-    method public java.util.UUID getId();
-    method public androidx.work.Data getInputData();
-    method @RequiresApi(28) public android.net.Network? getNetwork();
-    method @IntRange(from=0) public int getRunAttemptCount();
-    method public java.util.Set<java.lang.String!> getTags();
-    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
-    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
-  }
-
-}
-
-package androidx.work.multiprocess {
-
-  public abstract class RemoteWorkContinuation {
-    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
-    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
-  }
-
-  public abstract class RemoteWorkManager {
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
-    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
-    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
-    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
-  }
-
-}
-
diff --git a/work/workmanager/build.gradle b/work/workmanager/build.gradle
index d3576af..f6c9f47 100644
--- a/work/workmanager/build.gradle
+++ b/work/workmanager/build.gradle
@@ -49,11 +49,13 @@
 }
 
 dependencies {
+    implementation("androidx.core:core:1.5.0-beta01")
     annotationProcessor("androidx.room:room-compiler:2.2.5")
     implementation("androidx.room:room-runtime:2.2.5")
     androidTestImplementation("androidx.room:room-testing:2.2.5")
     implementation("androidx.sqlite:sqlite:2.1.0")
     implementation("androidx.sqlite:sqlite-framework:2.1.0")
+    api("androidx.annotation:annotation-experimental:1.0.0")
     api(libs.guavaListenableFuture)
     api("androidx.lifecycle:lifecycle-livedata:2.1.0")
     api("androidx.startup:startup-runtime:1.0.0")
diff --git a/work/workmanager/lint-baseline.xml b/work/workmanager/lint-baseline.xml
index a4fedbb..27ddc53 100644
--- a/work/workmanager/lint-baseline.xml
+++ b/work/workmanager/lint-baseline.xml
@@ -206,7 +206,7 @@
         errorLine2="                                                  ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="408"
+            line="429"
             column="51"/>
     </issue>
 
@@ -217,18 +217,40 @@
         errorLine2="                                               ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="443"
+            line="464"
             column="48"/>
     </issue>
 
     <issue
         id="ClassVerificationFailure"
+        message="This call references a method added in API level 30; however, the containing class androidx.work.impl.utils.ForceStopRunnable is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                        activityManager.getHistoricalProcessExitReasons("
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/work/impl/utils/ForceStopRunnable.java"
+            line="171"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 30; however, the containing class androidx.work.impl.utils.ForceStopRunnable is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                        if (info.getReason() == REASON_USER_REQUESTED) {"
+        errorLine2="                                 ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/work/impl/utils/ForceStopRunnable.java"
+            line="180"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
         message="This call references a method added in API level 19; however, the containing class androidx.work.impl.utils.ForceStopRunnable is reachable from earlier API levels and will fail run-time class verification."
         errorLine1="                alarmManager.setExact(RTC_WAKEUP, triggerAt, pendingIntent);"
         errorLine2="                             ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/utils/ForceStopRunnable.java"
-            line="303"
+            line="339"
             column="30"/>
     </issue>
 
@@ -349,7 +371,7 @@
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="104"
+            line="106"
             column="25"/>
     </issue>
 
@@ -360,7 +382,7 @@
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="111"
+            line="113"
             column="25"/>
     </issue>
 
@@ -371,7 +393,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="113"
+            line="115"
             column="21"/>
     </issue>
 
@@ -382,7 +404,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="114"
+            line="116"
             column="21"/>
     </issue>
 
@@ -393,7 +415,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="121"
+            line="123"
             column="21"/>
     </issue>
 
@@ -404,7 +426,18 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="122"
+            line="124"
+            column="21"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 31; however, the containing class androidx.work.impl.background.systemjob.SystemJobInfoConverter is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            builder.setExpedited(true);"
+        errorLine2="                    ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
+            line="130"
             column="21"/>
     </issue>
 
@@ -415,7 +448,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="132"
+            line="140"
             column="16"/>
     </issue>
 
@@ -426,7 +459,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="150"
+            line="158"
             column="21"/>
     </issue>
 
@@ -503,7 +536,7 @@
         errorLine2="                                                                      ~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/WorkManagerImpl.java"
-            line="761"
+            line="767"
             column="71"/>
     </issue>
 
@@ -514,7 +547,7 @@
         errorLine2="                                                       ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/WorkRequest.java"
-            line="174"
+            line="175"
             column="56"/>
     </issue>
 
@@ -525,7 +558,7 @@
         errorLine2="                                                          ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/WorkRequest.java"
-            line="251"
+            line="252"
             column="59"/>
     </issue>
 
@@ -536,7 +569,7 @@
         errorLine2="                                              ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/WorkRequest.java"
-            line="283"
+            line="284"
             column="47"/>
     </issue>
 
@@ -618,11 +651,11 @@
         errorLine2="                   ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="178"
+            line="188"
             column="20"/>
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="191"
+            line="201"
             column="17"
             message="Setter here"/>
     </issue>
@@ -931,7 +964,7 @@
         errorLine2="                                                    ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="407"
+            line="428"
             column="53"/>
     </issue>
 
@@ -942,7 +975,7 @@
         errorLine2="                                                 ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="442"
+            line="463"
             column="50"/>
     </issue>
 
@@ -1712,7 +1745,7 @@
         errorLine2="            ~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="77"
+            line="78"
             column="13"/>
     </issue>
 
@@ -1723,7 +1756,7 @@
         errorLine2="            ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="78"
+            line="79"
             column="13"/>
     </issue>
 
@@ -1734,7 +1767,7 @@
         errorLine2="            ~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="79"
+            line="80"
             column="13"/>
     </issue>
 
@@ -1745,7 +1778,7 @@
         errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="80"
+            line="81"
             column="13"/>
     </issue>
 
@@ -1756,7 +1789,7 @@
         errorLine2="                                 ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="180"
+            line="181"
             column="34"/>
     </issue>
 
@@ -1954,7 +1987,7 @@
         errorLine2="           ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="76"
+            line="77"
             column="12"/>
     </issue>
 
@@ -1965,7 +1998,7 @@
         errorLine2="               ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="365"
+            line="377"
             column="16"/>
     </issue>
 
@@ -1976,7 +2009,7 @@
         errorLine2="               ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="368"
+            line="380"
             column="16"/>
     </issue>
 
@@ -1987,7 +2020,7 @@
         errorLine2="               ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="395"
+            line="407"
             column="16"/>
     </issue>
 
@@ -1998,7 +2031,7 @@
         errorLine2="               ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="398"
+            line="410"
             column="16"/>
     </issue>
 
@@ -2009,7 +2042,7 @@
         errorLine2="               ~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="401"
+            line="413"
             column="16"/>
     </issue>
 
@@ -2020,7 +2053,7 @@
         errorLine2="               ~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="411"
+            line="423"
             column="16"/>
     </issue>
 
@@ -2031,7 +2064,7 @@
         errorLine2="               ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="420"
+            line="432"
             column="16"/>
     </issue>
 
@@ -2097,7 +2130,7 @@
         errorLine2="                                 ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="90"
+            line="100"
             column="34"/>
     </issue>
 
@@ -2108,7 +2141,7 @@
         errorLine2="                  ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="123"
+            line="133"
             column="19"/>
     </issue>
 
@@ -2119,7 +2152,7 @@
         errorLine2="                                         ~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="156"
+            line="166"
             column="42"/>
     </issue>
 
@@ -2130,7 +2163,7 @@
         errorLine2="                  ~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="177"
+            line="187"
             column="19"/>
     </issue>
 
@@ -2141,7 +2174,7 @@
         errorLine2="                                       ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="198"
+            line="208"
             column="40"/>
     </issue>
 
@@ -2152,7 +2185,7 @@
         errorLine2="                  ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="233"
+            line="243"
             column="19"/>
     </issue>
 
@@ -2163,7 +2196,7 @@
         errorLine2="                  ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="266"
+            line="315"
             column="19"/>
     </issue>
 
@@ -2174,7 +2207,7 @@
         errorLine2="                                                       ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="266"
+            line="315"
             column="56"/>
     </issue>
 
@@ -2185,7 +2218,7 @@
         errorLine2="                  ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="305"
+            line="354"
             column="19"/>
     </issue>
 
@@ -2196,7 +2229,7 @@
         errorLine2="                                                                   ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="305"
+            line="354"
             column="68"/>
     </issue>
 
@@ -2211,15 +2244,4 @@
             column="16"/>
     </issue>
 
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public WorkerWrapper build() {"
-        errorLine2="               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/WorkerWrapper.java"
-            line="684"
-            column="16"/>
-    </issue>
-
 </issues>
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index 4ecbbe1..a593404 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -19,6 +19,7 @@
 import static android.content.Context.MODE_PRIVATE;
 import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL;
 
+import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_11_12;
 import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
 import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_4_5;
 import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_6_7;
@@ -27,6 +28,7 @@
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_1;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_10;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_11;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_12;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_4;
@@ -85,6 +87,7 @@
     private static final String COLUMN_SYSTEM_ID = "system_id";
     private static final String COLUMN_ALARM_ID = "alarm_id";
     private static final String COLUMN_RUN_IN_FOREGROUND = "run_in_foreground";
+    private static final String COLUMN_OUT_OF_QUOTA_POLICY = "out_of_quota_policy";
 
     // Queries
     private static final String INSERT_ALARM_INFO = "INSERT INTO alarmInfo VALUES (?, ?)";
@@ -436,6 +439,22 @@
         database.close();
     }
 
+    @Test
+    @MediumTest
+    public void testMigrationVersion11To12() throws IOException {
+        SupportSQLiteDatabase database =
+                mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_11);
+        database = mMigrationTestHelper.runMigrationsAndValidate(
+                TEST_DATABASE,
+                VERSION_12,
+                VALIDATE_DROPPED_TABLES,
+                MIGRATION_11_12);
+
+        assertThat(checkColumnExists(database, TABLE_WORKSPEC, COLUMN_OUT_OF_QUOTA_POLICY),
+                is(true));
+        database.close();
+    }
+
     @NonNull
     private ContentValues contentValues(String workSpecId) {
         ContentValues contentValues = new ContentValues();
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt b/work/workmanager/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt
new file mode 100644
index 0000000..8babf02
--- /dev/null
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt
@@ -0,0 +1,179 @@
+/*
+ * 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 androidx.work
+
+import android.app.Notification
+import android.content.Context
+import android.util.Log
+import androidx.core.os.BuildCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.impl.utils.SynchronousExecutor
+import androidx.work.impl.utils.WorkForegroundRunnable
+import androidx.work.impl.utils.futures.SettableFuture
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.worker.TestWorker
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.util.UUID
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+public class WorkForegroundRunnableTest : DatabaseTest() {
+    private lateinit var context: Context
+    private lateinit var configuration: Configuration
+    private lateinit var executor: Executor
+    private lateinit var progressUpdater: ProgressUpdater
+    private lateinit var foregroundUpdater: ForegroundUpdater
+    private lateinit var taskExecutor: TaskExecutor
+
+    @Before
+    public fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().targetContext
+        executor = SynchronousExecutor()
+        configuration = Configuration.Builder()
+            .setMinimumLoggingLevel(Log.DEBUG)
+            .setExecutor(executor)
+            .build()
+        progressUpdater = mock(ProgressUpdater::class.java)
+        foregroundUpdater = mock(ForegroundUpdater::class.java)
+        taskExecutor = InstantWorkTaskExecutor()
+    }
+
+    @Test
+    @MediumTest
+    @SdkSuppress(maxSdkVersion = 30)
+    public fun doesNothing_forRegularWorkRequests() {
+        val work = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .build()
+
+        insertWork(work)
+        val worker = spy(
+            configuration.mWorkerFactory.createWorkerWithDefaultFallback(
+                context,
+                work.workSpec.workerClassName,
+                newWorkerParams(work)
+            )!!
+        )
+        val runnable = WorkForegroundRunnable(
+            context,
+            work.workSpec,
+            worker,
+            foregroundUpdater,
+            taskExecutor
+        )
+        runnable.run()
+        assertThat(runnable.future.isDone, `is`(equalTo(true)))
+        verifyNoMoreInteractions(foregroundUpdater)
+    }
+
+    @Test
+    @MediumTest
+    public fun callGetForeground_forExpeditedWork1() {
+        if (BuildCompat.isAtLeastS()) {
+            return
+        }
+
+        val work = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+            .build()
+
+        insertWork(work)
+        val worker = spy(
+            configuration.mWorkerFactory.createWorkerWithDefaultFallback(
+                context,
+                work.workSpec.workerClassName,
+                newWorkerParams(work)
+            )!!
+        )
+        val runnable = WorkForegroundRunnable(
+            context,
+            work.workSpec,
+            worker,
+            foregroundUpdater,
+            taskExecutor
+        )
+        runnable.run()
+        verify(worker).foregroundInfoAsync
+        assertThat(runnable.future.isDone, `is`(equalTo(true)))
+    }
+
+    @Test
+    @SmallTest
+    public fun callGetForeground_forExpeditedWork2() {
+        if (BuildCompat.isAtLeastS()) {
+            return
+        }
+
+        val work = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+            .build()
+
+        insertWork(work)
+        val worker = spy(
+            configuration.mWorkerFactory.createWorkerWithDefaultFallback(
+                context,
+                work.workSpec.workerClassName,
+                newWorkerParams(work)
+            )!!
+        )
+
+        val notification = mock(Notification::class.java)
+        val id = 10
+        val foregroundInfo = ForegroundInfo(id, notification)
+        val foregroundFuture = SettableFuture.create<ForegroundInfo>()
+        foregroundFuture.set(foregroundInfo)
+        `when`(worker.foregroundInfoAsync).thenReturn(foregroundFuture)
+        val runnable = WorkForegroundRunnable(
+            context,
+            work.workSpec,
+            worker,
+            foregroundUpdater,
+            taskExecutor
+        )
+        runnable.run()
+        verify(worker).foregroundInfoAsync
+        verify(foregroundUpdater).setForegroundAsync(context, work.id, foregroundInfo)
+        assertThat(runnable.future.isDone, `is`(equalTo(true)))
+    }
+
+    private fun newWorkerParams(workRequest: WorkRequest) = WorkerParameters(
+        UUID.fromString(workRequest.stringId),
+        Data.EMPTY,
+        listOf<String>(),
+        WorkerParameters.RuntimeExtras(),
+        1,
+        executor,
+        taskExecutor,
+        configuration.mWorkerFactory,
+        progressUpdater,
+        foregroundUpdater
+    )
+}
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java b/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java
index 5237099..4c526d8 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java
@@ -16,11 +16,15 @@
 
 package androidx.work;
 
+import static androidx.work.NetworkType.METERED;
+import static androidx.work.NetworkType.NOT_REQUIRED;
+
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.work.impl.model.WorkSpec;
@@ -139,4 +143,97 @@
                 .setInitialDelay(Long.MAX_VALUE - now, TimeUnit.MILLISECONDS)
                 .build();
     }
+
+    @Test
+    public void testBuild_expedited_noConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(NOT_REQUIRED));
+    }
+
+    @Test
+    public void testBuild_expedited_networkConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
+
+    @Test
+    public void testBuild_expedited_networkStorageConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .setRequiresStorageNotLow(true)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
+
+    @Test
+    public void testBuild_expedited_withUnspportedConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        mThrown.expect(IllegalArgumentException.class);
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .setRequiresStorageNotLow(true)
+                        .setRequiresCharging(true)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
+
+    @Test
+    public void testBuild_expedited_withUnspportedConstraints2() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        mThrown.expect(IllegalArgumentException.class);
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .setRequiresStorageNotLow(true)
+                        .setRequiresDeviceIdle(true)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
 }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index 32786ae..64a2074 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -35,6 +35,7 @@
 import android.net.Uri;
 import android.os.Build;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
@@ -241,6 +242,33 @@
         assertThat(jobInfo.isImportantWhileForeground(), is(false));
     }
 
+    @Test
+    @SmallTest
+    public void testConvert_expedited() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        WorkSpec workSpec = new WorkSpec("id", TestWorker.class.getName());
+        workSpec.expedited = true;
+        JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+        assertThat(jobInfo.isExpedited(), is(true));
+    }
+
+    @Test
+    @SmallTest
+    public void testConvertExpeditedJobs_retriesAreNotExpedited() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        WorkSpec workSpec = new WorkSpec("id", TestWorker.class.getName());
+        workSpec.expedited = true;
+        workSpec.runAttemptCount = 1; // retry
+        JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+        assertThat(jobInfo.isExpedited(), is(false));
+    }
+
     private void convertWithRequiredNetworkType(NetworkType networkType,
                                                 int jobInfoNetworkType,
                                                 int minSdkVersion) {
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt
index 9d10994..b9373a0 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt
@@ -21,25 +21,30 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
+import androidx.work.Constraints
 import androidx.work.ForegroundInfo
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkInfo
 import androidx.work.impl.WorkDatabase
 import androidx.work.impl.foreground.ForegroundProcessor
 import androidx.work.impl.model.WorkSpecDao
 import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
 import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.worker.TestWorker
+import org.junit.Assert.assertNotNull
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.mock
 import java.util.UUID
 
 @RunWith(AndroidJUnit4::class)
-// Mockito tries to class load android.os.CancellationSignal which is only available on API >= 16
-@SdkSuppress(minSdkVersion = 16)
-class WorkForegroundUpdaterTest {
+
+public class WorkForegroundUpdaterTest {
 
     private lateinit var mContext: Context
     private lateinit var mDatabase: WorkDatabase
@@ -49,7 +54,7 @@
     private lateinit var mForegroundInfo: ForegroundInfo
 
     @Before
-    fun setUp() {
+    public fun setUp() {
         mContext = mock(Context::class.java)
         mDatabase = mock(WorkDatabase::class.java)
         mWorkSpecDao = mock(WorkSpecDao::class.java)
@@ -62,7 +67,9 @@
 
     @Test(expected = IllegalStateException::class)
     @MediumTest
-    fun setForeground_whenWorkReplaced() {
+    // Mockito tries to class load android.os.CancellationSignal which is only available on API >= 16
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 29)
+    public fun setForeground_whenWorkReplaced() {
         val foregroundUpdater =
             WorkForegroundUpdater(mDatabase, mForegroundProcessor, mTaskExecutor)
         val uuid = UUID.randomUUID()
@@ -75,7 +82,9 @@
 
     @Test(expected = IllegalStateException::class)
     @MediumTest
-    fun setForeground_whenWorkFinished() {
+    // Mockito tries to class load android.os.CancellationSignal which is only available on API >= 16
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 29)
+    public fun setForeground_whenWorkFinished() {
         `when`(mWorkSpecDao.getState(anyString())).thenReturn(WorkInfo.State.SUCCEEDED)
         val foregroundUpdater =
             WorkForegroundUpdater(mDatabase, mForegroundProcessor, mTaskExecutor)
@@ -86,4 +95,29 @@
             throw exception.cause ?: exception
         }
     }
+
+    @Test
+    @MediumTest
+    public fun setForeground_throwsException() {
+        `when`(mWorkSpecDao.getState(anyString())).thenReturn(WorkInfo.State.RUNNING)
+        `when`(mForegroundProcessor.startForeground(anyString(), ArgumentMatchers.any()))
+            .thenThrow(IllegalStateException("Subject to foreground service restrictions."))
+        val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setConstraints(
+                Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
+            ).build()
+        val notificationId = 1
+        val notification = mock(Notification::class.java)
+        val metadata = ForegroundInfo(notificationId, notification)
+        val foregroundUpdater =
+            WorkForegroundUpdater(mDatabase, mForegroundProcessor, mTaskExecutor)
+        var exception: Throwable? = null
+        try {
+            val future = foregroundUpdater.setForegroundAsync(mContext, request.id, metadata)
+            future.get()
+        } catch (throwable: Throwable) {
+            exception = throwable
+        }
+        assertNotNull("Exception cannot be null", exception)
+    }
 }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt b/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt
index 9197f1d..0c12308 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt
@@ -21,6 +21,8 @@
 import androidx.work.ForegroundInfo
 import androidx.work.Worker
 import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
 
 public open class StopAwareForegroundWorker(
     private val context: Context,
@@ -29,13 +31,18 @@
     Worker(context, parameters) {
 
     override fun doWork(): Result {
-        setForegroundAsync(getNotification())
         while (!isStopped) {
             // Do nothing
         }
         return Result.success()
     }
 
+    override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+        val future = SettableFuture.create<ForegroundInfo>()
+        future.set(getNotification())
+        return future
+    }
+
     private fun getNotification(): ForegroundInfo {
         val notification = NotificationCompat.Builder(context, ChannelId)
             .setOngoing(true)
diff --git a/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt b/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt
index d75a5ec..2968cf7 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt
@@ -21,6 +21,8 @@
 import androidx.work.ForegroundInfo
 import androidx.work.Worker
 import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
 
 public open class TestForegroundWorker(
     private val context: Context,
@@ -29,10 +31,15 @@
     Worker(context, parameters) {
 
     override fun doWork(): Result {
-        setForegroundAsync(getNotification()).get()
         return Result.success()
     }
 
+    override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+        val future = SettableFuture.create<ForegroundInfo>()
+        future.set(getNotification())
+        return future
+    }
+
     private fun getNotification(): ForegroundInfo {
         val notification = NotificationCompat.Builder(context, ChannelId)
             .setOngoing(true)
diff --git a/work/workmanager/src/main/java/androidx/work/Constraints.java b/work/workmanager/src/main/java/androidx/work/Constraints.java
index ad6d5d0..ffca8e3 100644
--- a/work/workmanager/src/main/java/androidx/work/Constraints.java
+++ b/work/workmanager/src/main/java/androidx/work/Constraints.java
@@ -290,6 +290,27 @@
         long mTriggerContentMaxDelay = -1;
         ContentUriTriggers mContentUriTriggers = new ContentUriTriggers();
 
+        public Builder() {
+            // default public constructor
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(@NonNull Constraints constraints) {
+            mRequiresCharging = constraints.requiresCharging();
+            mRequiresDeviceIdle = Build.VERSION.SDK_INT >= 23 && constraints.requiresDeviceIdle();
+            mRequiredNetworkType = constraints.getRequiredNetworkType();
+            mRequiresBatteryNotLow = constraints.requiresBatteryNotLow();
+            mRequiresStorageNotLow = constraints.requiresStorageNotLow();
+            if (Build.VERSION.SDK_INT >= 24) {
+                mTriggerContentUpdateDelay = constraints.getTriggerContentUpdateDelay();
+                mTriggerContentMaxDelay = constraints.getTriggerMaxContentDelay();
+                mContentUriTriggers = constraints.getContentUriTriggers();
+            }
+        }
+
         /**
          * Sets whether device should be charging for the {@link WorkRequest} to run.  The
          * default value is {@code false}.
diff --git a/work/workmanager/src/main/java/androidx/work/ExperimentalExpeditedWork.java b/work/workmanager/src/main/java/androidx/work/ExperimentalExpeditedWork.java
new file mode 100644
index 0000000..0829924
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/ExperimentalExpeditedWork.java
@@ -0,0 +1,39 @@
+/*
+ * 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 androidx.work;
+
+import static androidx.annotation.experimental.Experimental.Level.ERROR;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.experimental.Experimental;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * An API surface for expedited {@link WorkRequest}s.
+ */
+@Retention(CLASS)
+@Target({TYPE, METHOD, PACKAGE})
+@Experimental(level = ERROR)
+public @interface ExperimentalExpeditedWork {
+
+}
diff --git a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
index f57c3e6..bf615d5 100644
--- a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
+++ b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.work.impl.utils.futures.SettableFuture;
 import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -216,6 +217,12 @@
      * Under the hood, WorkManager manages and runs a foreground service on your behalf to
      * execute this WorkRequest, showing the notification provided in
      * {@link ForegroundInfo}.
+     * <p>
+     * Calling {@code setForegroundAsync} will fail with an
+     * {@link IllegalStateException} when the process is subject to foreground
+     * service restrictions. Consider using
+     * {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)} and
+     * {@link ListenableWorker#getForegroundInfoAsync()} instead.
      *
      * @param foregroundInfo The {@link ForegroundInfo}
      * @return A {@link ListenableFuture} which resolves after the {@link ListenableWorker}
@@ -229,11 +236,36 @@
     }
 
     /**
+     * Return an instance of {@link  ForegroundInfo} if the {@link WorkRequest} is important to
+     * the user.  In this case, WorkManager provides a signal to the OS that the process should
+     * be kept alive while this work is executing.
+     * <p>
+     * Prior to Android S, WorkManager manages and runs a foreground service on your behalf to
+     * execute the WorkRequest, showing the notification provided in the {@link ForegroundInfo}.
+     * To update this notification subsequently, the application can use
+     * {@link android.app.NotificationManager}.
+     * <p>
+     * Starting in Android S and above, WorkManager manages this WorkRequest using an immediate job.
+     *
+     * @return A {@link ListenableFuture} of {@link ForegroundInfo} instance if the WorkRequest
+     * is marked immediate. For more information look at
+     * {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)}.
+     */
+    @NonNull
+    @ExperimentalExpeditedWork
+    public ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
+        SettableFuture<ForegroundInfo> future = SettableFuture.create();
+        future.setException(new IllegalStateException("Not implemented"));
+        return future;
+    }
+
+    /**
      * Returns {@code true} if this Worker has been told to stop.  This could be because of an
      * explicit cancellation signal by the user, or because the system has decided to preempt the
      * task. In these cases, the results of the work will be ignored by WorkManager and it is safe
      * to stop the computation.  WorkManager will retry the work at a later time if necessary.
      *
+     *
      * @return {@code true} if the work operation has been interrupted
      */
     public final boolean isStopped() {
@@ -297,6 +329,14 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public void setRunInForeground(boolean runInForeground) {
+        mRunInForeground = runInForeground;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public @NonNull Executor getBackgroundExecutor() {
         return mWorkerParams.getBackgroundExecutor();
     }
diff --git a/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java b/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
index 51ba3e6..d33851e 100644
--- a/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
@@ -107,12 +107,6 @@
                 throw new IllegalArgumentException(
                         "Cannot set backoff criteria on an idle mode job");
             }
-            if (mWorkSpec.runInForeground
-                    && Build.VERSION.SDK_INT >= 23
-                    && mWorkSpec.constraints.requiresDeviceIdle()) {
-                throw new IllegalArgumentException(
-                        "Cannot run in foreground with an idle mode constraint");
-            }
             return new OneTimeWorkRequest(this);
         }
 
diff --git a/work/workmanager/src/main/java/androidx/work/OutOfQuotaPolicy.java b/work/workmanager/src/main/java/androidx/work/OutOfQuotaPolicy.java
new file mode 100644
index 0000000..5f25216
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/OutOfQuotaPolicy.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 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 androidx.work;
+
+/**
+ * An enumeration of policies that help determine out of quota behavior for expedited jobs.
+ */
+@ExperimentalExpeditedWork
+public enum OutOfQuotaPolicy {
+
+    /**
+     * When the app does not have any expedited job quota, the expedited work request will
+     * fallback to a regular work request.
+     */
+    RUN_AS_NON_EXPEDITED_WORK_REQUEST,
+
+    /**
+     * When the app does not have any expedited job quota, the expedited work request will
+     * we dropped and no work requests are enqueued.
+     */
+    DROP_WORK_REQUEST;
+}
diff --git a/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java b/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
index 6fd553a..fb44604 100644
--- a/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
@@ -189,12 +189,6 @@
                 throw new IllegalArgumentException(
                         "Cannot set backoff criteria on an idle mode job");
             }
-            if (mWorkSpec.runInForeground
-                    && Build.VERSION.SDK_INT >= 23
-                    && mWorkSpec.constraints.requiresDeviceIdle()) {
-                throw new IllegalArgumentException(
-                        "Cannot run in foreground with an idle mode constraint");
-            }
             return new PeriodicWorkRequest(this);
         }
 
diff --git a/work/workmanager/src/main/java/androidx/work/WorkRequest.java b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
index 25fed73..247ed84 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
@@ -16,6 +16,7 @@
 package androidx.work;
 
 import android.annotation.SuppressLint;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
@@ -289,12 +290,38 @@
         }
 
         /**
+         * Marks the {@link WorkRequest} as important to the user.  In this case, WorkManager
+         * provides an additional signal to the OS that this work is important.
+         *
+         * @param policy The {@link OutOfQuotaPolicy} to be used.
+         */
+        @ExperimentalExpeditedWork
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public @NonNull B setExpedited(@NonNull OutOfQuotaPolicy policy) {
+            mWorkSpec.expedited = true;
+            mWorkSpec.outOfQuotaPolicy = policy;
+            return getThis();
+        }
+
+        /**
          * Builds a {@link WorkRequest} based on this {@link Builder}.
          *
          * @return A {@link WorkRequest} based on this {@link Builder}
          */
         public final @NonNull W build() {
             W returnValue = buildInternal();
+            Constraints constraints = mWorkSpec.constraints;
+            // Check for unsupported constraints.
+            boolean hasUnsupportedConstraints =
+                    (Build.VERSION.SDK_INT >= 24 && constraints.hasContentUriTriggers())
+                            || constraints.requiresBatteryNotLow()
+                            || constraints.requiresCharging()
+                            || (Build.VERSION.SDK_INT >= 23 && constraints.requiresDeviceIdle());
+
+            if (mWorkSpec.expedited && hasUnsupportedConstraints) {
+                throw new IllegalArgumentException(
+                        "Expedited jobs only support network and storage constraints");
+            }
             // Create a new id and WorkSpec so this WorkRequest.Builder can be used multiple times.
             mId = UUID.randomUUID();
             mWorkSpec = new WorkSpec(mWorkSpec);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
index cccc41a..5073e66 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
@@ -75,7 +75,7 @@
         WorkName.class,
         WorkProgress.class,
         Preference.class},
-        version = 11)
+        version = 12)
 @TypeConverters(value = {Data.class, WorkTypeConverters.class})
 public abstract class WorkDatabase extends RoomDatabase {
     // Delete rows in the workspec table that...
@@ -150,6 +150,7 @@
                 .addMigrations(
                         new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_10,
                                 VERSION_11))
+                .addMigrations(WorkDatabaseMigrations.MIGRATION_11_12)
                 .fallbackToDestructiveMigration()
                 .build();
     }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
index 9261f37..7a1d045 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
@@ -59,6 +59,7 @@
     public static final int VERSION_9 = 9;
     public static final int VERSION_10 = 10;
     public static final int VERSION_11 = 11;
+    public static final int VERSION_12 = 12;
 
     private static final String CREATE_SYSTEM_ID_INFO =
             "CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id`"
@@ -106,6 +107,9 @@
             "CREATE TABLE IF NOT EXISTS `Preference` (`key` TEXT NOT NULL, `long_value` INTEGER, "
                     + "PRIMARY KEY(`key`))";
 
+    private static final String CREATE_OUT_OF_QUOTA_POLICY =
+            "ALTER TABLE workspec ADD COLUMN `out_of_quota_policy` INTEGER NOT NULL DEFAULT 0";
+
     /**
      * Removes the {@code alarmInfo} table and substitutes it for a more general
      * {@code SystemIdInfo} table.
@@ -228,4 +232,15 @@
             IdGenerator.migrateLegacyIdGenerator(mContext, database);
         }
     }
+
+    /**
+     * Adds a notification_provider to the {@link WorkSpec}.
+     */
+    @NonNull
+    public static Migration MIGRATION_11_12 = new Migration(VERSION_11, VERSION_12) {
+        @Override
+        public void migrate(@NonNull SupportSQLiteDatabase database) {
+            database.execSQL(CREATE_OUT_OF_QUOTA_POLICY);
+        }
+    };
 }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 7328ba6..ed8718c 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -16,6 +16,7 @@
 
 package androidx.work.impl;
 
+import static android.app.PendingIntent.FLAG_MUTABLE;
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 import static android.text.TextUtils.isEmpty;
 
@@ -31,6 +32,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.arch.core.util.Function;
+import androidx.core.os.BuildCompat;
 import androidx.lifecycle.LiveData;
 import androidx.work.Configuration;
 import androidx.work.ExistingPeriodicWorkPolicy;
@@ -475,7 +477,11 @@
     @Override
     public PendingIntent createCancelPendingIntent(@NonNull UUID id) {
         Intent intent = createCancelWorkIntent(mContext, id.toString());
-        return PendingIntent.getService(mContext, 0, intent, FLAG_UPDATE_CURRENT);
+        int flags = FLAG_UPDATE_CURRENT;
+        if (BuildCompat.isAtLeastS()) {
+            flags |= FLAG_MUTABLE;
+        }
+        return PendingIntent.getService(mContext, 0, intent, flags);
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
index 26654a4..12b8eee 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -48,6 +48,7 @@
 import androidx.work.impl.model.WorkSpecDao;
 import androidx.work.impl.model.WorkTagDao;
 import androidx.work.impl.utils.PackageManagerHelper;
+import androidx.work.impl.utils.WorkForegroundRunnable;
 import androidx.work.impl.utils.WorkForegroundUpdater;
 import androidx.work.impl.utils.WorkProgressUpdater;
 import androidx.work.impl.utils.futures.SettableFuture;
@@ -82,13 +83,13 @@
     // Avoid Synthetic accessor
     WorkSpec mWorkSpec;
     ListenableWorker mWorker;
+    TaskExecutor mWorkTaskExecutor;
 
     // Package-private for synthetic accessor.
     @NonNull
     ListenableWorker.Result mResult = ListenableWorker.Result.failure();
 
     private Configuration mConfiguration;
-    private TaskExecutor mWorkTaskExecutor;
     private ForegroundProcessor mForegroundProcessor;
     private WorkDatabase mWorkDatabase;
     private WorkSpecDao mWorkSpecDao;
@@ -226,7 +227,7 @@
             input = inputMerger.merge(inputs);
         }
 
-        WorkerParameters params = new WorkerParameters(
+        final WorkerParameters params = new WorkerParameters(
                 UUID.fromString(mWorkSpecId),
                 input,
                 mTags,
@@ -272,22 +273,32 @@
             }
 
             final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
-            // Call mWorker.startWork() on the main thread.
-            mWorkTaskExecutor.getMainThreadExecutor()
-                    .execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                Logger.get().debug(TAG, String.format("Starting work for %s",
-                                        mWorkSpec.workerClassName));
-                                mInnerFuture = mWorker.startWork();
-                                future.setFuture(mInnerFuture);
-                            } catch (Throwable e) {
-                                future.setException(e);
-                            }
+            final WorkForegroundRunnable foregroundRunnable =
+                    new WorkForegroundRunnable(
+                            mAppContext,
+                            mWorkSpec,
+                            mWorker,
+                            params.getForegroundUpdater(),
+                            mWorkTaskExecutor
+                    );
+            mWorkTaskExecutor.getMainThreadExecutor().execute(foregroundRunnable);
 
-                        }
-                    });
+            final ListenableFuture<Void> runExpedited = foregroundRunnable.getFuture();
+            runExpedited.addListener(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        runExpedited.get();
+                        Logger.get().debug(TAG,
+                                String.format("Starting work for %s", mWorkSpec.workerClassName));
+                        // Call mWorker.startWork() on the main thread.
+                        mInnerFuture = mWorker.startWork();
+                        future.setFuture(mInnerFuture);
+                    } catch (Throwable e) {
+                        future.setException(e);
+                    }
+                }
+            }, mWorkTaskExecutor.getMainThreadExecutor());
 
             // Avoid synthetic accessors.
             final String workDescription = mWorkDescription;
@@ -681,6 +692,7 @@
         /**
          * @return The instance of {@link WorkerWrapper}.
          */
+        @NonNull
         public WorkerWrapper build() {
             return new WorkerWrapper(this);
         }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index 2541438..02df55e 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -30,6 +30,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.work.BackoffPolicy;
 import androidx.work.Constraints;
 import androidx.work.ContentUriTriggers;
@@ -65,7 +66,7 @@
      * Note: All {@link JobInfo} are set to persist on reboot.
      *
      * @param workSpec The {@link WorkSpec} to convert
-     * @param jobId The {@code jobId} to use. This is useful when de-duping jobs on reschedule.
+     * @param jobId    The {@code jobId} to use. This is useful when de-duping jobs on reschedule.
      * @return The {@link JobInfo} representing the same information as the {@link WorkSpec}
      */
     JobInfo convert(WorkSpec workSpec, int jobId) {
@@ -96,11 +97,12 @@
             // always setMinimumLatency to make sure we have at least one constraint.
             // See aosp/5434530 & b/6771687
             builder.setMinimumLatency(offset);
-        } else  {
+        } else {
             if (offset > 0) {
                 // Only set a minimum latency when applicable.
                 builder.setMinimumLatency(offset);
-            } else {
+            } else if (!workSpec.expedited) {
+                // Only set this if the workSpec is not expedited.
                 builder.setImportantWhileForeground(true);
             }
         }
@@ -121,6 +123,12 @@
             builder.setRequiresBatteryNotLow(constraints.requiresBatteryNotLow());
             builder.setRequiresStorageNotLow(constraints.requiresStorageNotLow());
         }
+        // Retries cannot be expedited jobs, given they will occur at some point in the future.
+        boolean isRetry = workSpec.runAttemptCount > 0;
+        if (BuildCompat.isAtLeastS() && workSpec.expedited && !isRetry) {
+            //noinspection NewApi
+            builder.setExpedited(true);
+        }
         return builder.build();
     }
 
@@ -161,7 +169,7 @@
      */
     @SuppressWarnings("MissingCasesInEnumSwitch")
     static int convertNetworkType(NetworkType networkType) {
-        switch(networkType) {
+        switch (networkType) {
             case NOT_REQUIRED:
                 return JobInfo.NETWORK_TYPE_NONE;
             case CONNECTED:
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index 6e39078..dcbb0d5e 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -17,6 +17,7 @@
 
 import static android.content.Context.JOB_SCHEDULER_SERVICE;
 
+import static androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
 import static androidx.work.impl.background.systemjob.SystemJobInfoConverter.EXTRA_WORK_SPEC_ID;
 import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
 
@@ -183,7 +184,20 @@
                 TAG,
                 String.format("Scheduling work ID %s Job ID %s", workSpec.id, jobId));
         try {
-            mJobScheduler.schedule(jobInfo);
+            int result = mJobScheduler.schedule(jobInfo);
+            if (result == JobScheduler.RESULT_FAILURE) {
+                Logger.get()
+                        .warning(TAG, String.format("Unable to schedule work ID %s", workSpec.id));
+                if (workSpec.expedited
+                        && workSpec.outOfQuotaPolicy == RUN_AS_NON_EXPEDITED_WORK_REQUEST) {
+                    // Falling back to a non-expedited job.
+                    workSpec.expedited = false;
+                    String message = String.format(
+                            "Scheduling a non-expedited job (work ID %s)", workSpec.id);
+                    Logger.get().debug(TAG, message);
+                    scheduleInternal(workSpec, jobId);
+                }
+            }
         } catch (IllegalStateException e) {
             // This only gets thrown if we exceed 100 jobs.  Let's figure out if WorkManager is
             // responsible for all these jobs.
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
index 86bfa80..245f432 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
@@ -36,6 +36,7 @@
 import androidx.work.Constraints;
 import androidx.work.Data;
 import androidx.work.Logger;
+import androidx.work.OutOfQuotaPolicy;
 import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
 
@@ -129,10 +130,19 @@
     public long scheduleRequestedAt = SCHEDULE_NOT_REQUESTED_YET;
 
     /**
-     * This is {@code true} when the WorkSpec needs to be hosted by a foreground service.
+     * This is {@code true} when the WorkSpec needs to be hosted by a foreground service or a
+     * high priority job.
      */
     @ColumnInfo(name = "run_in_foreground")
-    public boolean runInForeground;
+    public boolean expedited;
+
+    /**
+     * When set to <code>true</code> this {@link WorkSpec} falls back to a regular job when
+     * an application runs out of expedited job quota.
+     */
+    @NonNull
+    @ColumnInfo(name = "out_of_quota_policy")
+    public OutOfQuotaPolicy outOfQuotaPolicy = OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
 
     public WorkSpec(@NonNull String id, @NonNull String workerClassName) {
         this.id = id;
@@ -156,7 +166,8 @@
         periodStartTime = other.periodStartTime;
         minimumRetentionDuration = other.minimumRetentionDuration;
         scheduleRequestedAt = other.scheduleRequestedAt;
-        runInForeground = other.runInForeground;
+        expedited = other.expedited;
+        outOfQuotaPolicy = other.outOfQuotaPolicy;
     }
 
     /**
@@ -174,7 +185,6 @@
         this.backoffDelayDuration = backoffDelayDuration;
     }
 
-
     public boolean isPeriodic() {
         return intervalDuration != 0L;
     }
@@ -301,7 +311,7 @@
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
-        if (!(o instanceof WorkSpec)) return false;
+        if (o == null || getClass() != o.getClass()) return false;
 
         WorkSpec workSpec = (WorkSpec) o;
 
@@ -313,7 +323,7 @@
         if (periodStartTime != workSpec.periodStartTime) return false;
         if (minimumRetentionDuration != workSpec.minimumRetentionDuration) return false;
         if (scheduleRequestedAt != workSpec.scheduleRequestedAt) return false;
-        if (runInForeground != workSpec.runInForeground) return false;
+        if (expedited != workSpec.expedited) return false;
         if (!id.equals(workSpec.id)) return false;
         if (state != workSpec.state) return false;
         if (!workerClassName.equals(workSpec.workerClassName)) return false;
@@ -325,7 +335,8 @@
         if (!input.equals(workSpec.input)) return false;
         if (!output.equals(workSpec.output)) return false;
         if (!constraints.equals(workSpec.constraints)) return false;
-        return backoffPolicy == workSpec.backoffPolicy;
+        if (backoffPolicy != workSpec.backoffPolicy) return false;
+        return outOfQuotaPolicy == workSpec.outOfQuotaPolicy;
     }
 
     @Override
@@ -346,7 +357,8 @@
         result = 31 * result + (int) (periodStartTime ^ (periodStartTime >>> 32));
         result = 31 * result + (int) (minimumRetentionDuration ^ (minimumRetentionDuration >>> 32));
         result = 31 * result + (int) (scheduleRequestedAt ^ (scheduleRequestedAt >>> 32));
-        result = 31 * result + (runInForeground ? 1 : 0);
+        result = 31 * result + (expedited ? 1 : 0);
+        result = 31 * result + outOfQuotaPolicy.hashCode();
         return result;
     }
 
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
index fb15ba8..f60ac02 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
@@ -28,10 +28,12 @@
 import android.net.Uri;
 import android.os.Build;
 
+import androidx.annotation.NonNull;
 import androidx.room.TypeConverter;
 import androidx.work.BackoffPolicy;
 import androidx.work.ContentUriTriggers;
 import androidx.work.NetworkType;
+import androidx.work.OutOfQuotaPolicy;
 import androidx.work.WorkInfo;
 
 import java.io.ByteArrayInputStream;
@@ -81,6 +83,14 @@
     }
 
     /**
+     * Integer identifiers that map to {@link OutOfQuotaPolicy}.
+     */
+    public interface OutOfPolicyIds {
+        int RUN_AS_NON_EXPEDITED_WORK_REQUEST = 0;
+        int DROP_WORK_REQUEST = 1;
+    }
+
+    /**
      * TypeConverter for a State to an int.
      *
      * @param state The input State
@@ -257,6 +267,45 @@
     }
 
     /**
+     * Converts a {@link OutOfQuotaPolicy} to an int.
+     *
+     * @param policy The {@link OutOfQuotaPolicy} policy being used
+     * @return the corresponding int representation.
+     */
+    @TypeConverter
+    public static int outOfQuotaPolicyToInt(@NonNull OutOfQuotaPolicy policy) {
+        switch (policy) {
+            case RUN_AS_NON_EXPEDITED_WORK_REQUEST:
+                return OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+            case DROP_WORK_REQUEST:
+                return OutOfPolicyIds.DROP_WORK_REQUEST;
+            default:
+                throw new IllegalArgumentException(
+                        "Could not convert " + policy + " to int");
+        }
+    }
+
+    /**
+     * Converter from an int to a {@link OutOfQuotaPolicy}.
+     *
+     * @param value The input integer
+     * @return An {@link OutOfQuotaPolicy}
+     */
+    @TypeConverter
+    @NonNull
+    public static OutOfQuotaPolicy intToOutOfQuotaPolicy(int value) {
+        switch (value) {
+            case OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST:
+                return OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+            case OutOfPolicyIds.DROP_WORK_REQUEST:
+                return OutOfQuotaPolicy.DROP_WORK_REQUEST;
+            default:
+                throw new IllegalArgumentException(
+                        "Could not convert " + value + " to OutOfQuotaPolicy");
+        }
+    }
+
+    /**
      * Converts a list of {@link ContentUriTriggers.Trigger}s to byte array representation
      * @param triggers the list of {@link ContentUriTriggers.Trigger}s to convert
      * @return corresponding byte array representation
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 4a31178..55824bc 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -17,13 +17,17 @@
 package androidx.work.impl.utils;
 
 import static android.app.AlarmManager.RTC_WAKEUP;
+import static android.app.ApplicationExitInfo.REASON_USER_REQUESTED;
+import static android.app.PendingIntent.FLAG_MUTABLE;
 import static android.app.PendingIntent.FLAG_NO_CREATE;
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 
 import static androidx.work.WorkInfo.State.ENQUEUED;
 import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
 
+import android.app.ActivityManager;
 import android.app.AlarmManager;
+import android.app.ApplicationExitInfo;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -40,6 +44,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.work.Configuration;
 import androidx.work.InitializationExceptionHandler;
 import androidx.work.Logger;
@@ -155,18 +160,46 @@
         // Even though API 23, 24 are probably safe, OEMs may choose to do
         // something different.
         try {
-            PendingIntent pendingIntent = getPendingIntent(mContext, FLAG_NO_CREATE);
-            if (pendingIntent == null) {
+            int flags = FLAG_NO_CREATE;
+            if (BuildCompat.isAtLeastS()) {
+                flags |= FLAG_MUTABLE;
+            }
+            PendingIntent pendingIntent = getPendingIntent(mContext, flags);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                // We no longer need the alarm.
+                if (pendingIntent != null) {
+                    pendingIntent.cancel();
+                }
+                ActivityManager activityManager =
+                        (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+                List<ApplicationExitInfo> exitInfoList =
+                        activityManager.getHistoricalProcessExitReasons(
+                                null /* match caller uid */,
+                                0, // ignore
+                                0 // ignore
+                        );
+
+                if (exitInfoList != null && !exitInfoList.isEmpty()) {
+                    for (int i = 0; i < exitInfoList.size(); i++) {
+                        ApplicationExitInfo info = exitInfoList.get(i);
+                        if (info.getReason() == REASON_USER_REQUESTED) {
+                            return true;
+                        }
+                    }
+                }
+            } else if (pendingIntent == null) {
                 setAlarm(mContext);
                 return true;
-            } else {
-                return false;
             }
-        } catch (SecurityException exception) {
+            return false;
+        } catch (SecurityException | IllegalArgumentException exception) {
+            // b/189975360 Some Samsung Devices seem to throw an IllegalArgumentException :( on
+            // API 30.
+
             // Setting Alarms on some devices fails due to OEM introduced bugs in AlarmManager.
             // When this happens, there is not much WorkManager can do, other can reschedule
             // everything.
-            Logger.get().warning(TAG, "Ignoring security exception", exception);
+            Logger.get().warning(TAG, "Ignoring exception", exception);
             return true;
         }
     }
@@ -299,7 +332,11 @@
     static void setAlarm(Context context) {
         AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         // Using FLAG_UPDATE_CURRENT, because we only ever want once instance of this alarm.
-        PendingIntent pendingIntent = getPendingIntent(context, FLAG_UPDATE_CURRENT);
+        int flags = FLAG_UPDATE_CURRENT;
+        if (BuildCompat.isAtLeastS()) {
+            flags |= FLAG_MUTABLE;
+        }
+        PendingIntent pendingIntent = getPendingIntent(context, flags);
         long triggerAt = System.currentTimeMillis() + TEN_YEARS;
         if (alarmManager != null) {
             if (Build.VERSION.SDK_INT >= 19) {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundRunnable.java
new file mode 100644
index 0000000..dad9d9d
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundRunnable.java
@@ -0,0 +1,113 @@
+/*
+ * 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 androidx.work.impl.utils;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+import androidx.work.ForegroundInfo;
+import androidx.work.ForegroundUpdater;
+import androidx.work.ListenableWorker;
+import androidx.work.Logger;
+import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.futures.SettableFuture;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class WorkForegroundRunnable implements Runnable {
+
+    // Synthetic access
+    static final String TAG = Logger.tagWithPrefix("WorkForegroundRunnable");
+
+    final SettableFuture<Void> mFuture;
+
+    final Context mContext;
+    final WorkSpec mWorkSpec;
+    final ListenableWorker mWorker;
+    final ForegroundUpdater mForegroundUpdater;
+    final TaskExecutor mTaskExecutor;
+
+    @SuppressLint("LambdaLast")
+    public WorkForegroundRunnable(
+            @NonNull Context context,
+            @NonNull WorkSpec workSpec,
+            @NonNull ListenableWorker worker,
+            @NonNull ForegroundUpdater foregroundUpdater,
+            @NonNull TaskExecutor taskExecutor) {
+
+        mFuture = SettableFuture.create();
+        mContext = context;
+        mWorkSpec = workSpec;
+        mWorker = worker;
+        mForegroundUpdater = foregroundUpdater;
+        mTaskExecutor = taskExecutor;
+    }
+
+    @NonNull
+    public ListenableFuture<Void> getFuture() {
+        return mFuture;
+    }
+
+    @Override
+    @SuppressLint("UnsafeExperimentalUsageError")
+    public void run() {
+        if (!mWorkSpec.expedited || BuildCompat.isAtLeastS()) {
+            mFuture.set(null);
+            return;
+        }
+
+        final SettableFuture<ForegroundInfo> foregroundFuture = SettableFuture.create();
+        mTaskExecutor.getMainThreadExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                foregroundFuture.setFuture(mWorker.getForegroundInfoAsync());
+            }
+        });
+
+        foregroundFuture.addListener(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    ForegroundInfo foregroundInfo = foregroundFuture.get();
+                    if (foregroundInfo == null) {
+                        String message =
+                                String.format("Worker was marked important (%s) but did not "
+                                        + "provide ForegroundInfo", mWorkSpec.workerClassName);
+                        throw new IllegalStateException(message);
+                    }
+                    Logger.get().debug(TAG, String.format("Updating notification for %s",
+                            mWorkSpec.workerClassName));
+                    // Mark as running in the foreground
+                    mWorker.setRunInForeground(true);
+                    mFuture.setFuture(
+                            mForegroundUpdater.setForegroundAsync(
+                                    mContext, mWorker.getId(), foregroundInfo));
+                } catch (Throwable throwable) {
+                    mFuture.setException(throwable);
+                }
+            }
+        }, mTaskExecutor.getMainThreadExecutor());
+    }
+}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
index 8e0ac60..743ae88 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
@@ -25,6 +25,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.work.ForegroundInfo;
 import androidx.work.ForegroundUpdater;
+import androidx.work.Logger;
 import androidx.work.WorkInfo;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.foreground.ForegroundProcessor;
@@ -46,6 +47,8 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class WorkForegroundUpdater implements ForegroundUpdater {
 
+    private static final String TAG = Logger.tagWithPrefix("WMFgUpdater");
+
     private final TaskExecutor mTaskExecutor;
 
     // Synthetic access
@@ -92,6 +95,8 @@
                         }
 
                         // startForeground() is idempotent
+                        // NOTE: This will fail when the process is subject to foreground service
+                        // restrictions. Propagate the exception to the caller.
                         mForegroundProcessor.startForeground(workSpecId, foregroundInfo);
                         Intent intent = createNotifyIntent(context, workSpecId, foregroundInfo);
                         context.startService(intent);
diff --git a/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/12.json b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/12.json
new file mode 100644
index 0000000..dc1d4dd
--- /dev/null
+++ b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/12.json
@@ -0,0 +1,460 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 12,
+    "identityHash": "c103703e120ae8cc73c9248622f3cd1e",
+    "entities": [
+      {
+        "tableName": "Dependency",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `prerequisite_id` TEXT NOT NULL, PRIMARY KEY(`work_spec_id`, `prerequisite_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`prerequisite_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "prerequisiteId",
+            "columnName": "prerequisite_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "work_spec_id",
+            "prerequisite_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_Dependency_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Dependency_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          },
+          {
+            "name": "index_Dependency_prerequisite_id",
+            "unique": false,
+            "columnNames": [
+              "prerequisite_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Dependency_prerequisite_id` ON `${TABLE_NAME}` (`prerequisite_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          },
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "prerequisite_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkSpec",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `state` INTEGER NOT NULL, `worker_class_name` TEXT NOT NULL, `input_merger_class_name` TEXT, `input` BLOB NOT NULL, `output` BLOB NOT NULL, `initial_delay` INTEGER NOT NULL, `interval_duration` INTEGER NOT NULL, `flex_duration` INTEGER NOT NULL, `run_attempt_count` INTEGER NOT NULL, `backoff_policy` INTEGER NOT NULL, `backoff_delay_duration` INTEGER NOT NULL, `period_start_time` INTEGER NOT NULL, `minimum_retention_duration` INTEGER NOT NULL, `schedule_requested_at` INTEGER NOT NULL, `run_in_foreground` INTEGER NOT NULL, `out_of_quota_policy` INTEGER NOT NULL, `required_network_type` INTEGER, `requires_charging` INTEGER NOT NULL, `requires_device_idle` INTEGER NOT NULL, `requires_battery_not_low` INTEGER NOT NULL, `requires_storage_not_low` INTEGER NOT NULL, `trigger_content_update_delay` INTEGER NOT NULL, `trigger_max_content_delay` INTEGER NOT NULL, `content_uri_triggers` BLOB, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "state",
+            "columnName": "state",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workerClassName",
+            "columnName": "worker_class_name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "inputMergerClassName",
+            "columnName": "input_merger_class_name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "input",
+            "columnName": "input",
+            "affinity": "BLOB",
+            "notNull": true
+          },
+          {
+            "fieldPath": "output",
+            "columnName": "output",
+            "affinity": "BLOB",
+            "notNull": true
+          },
+          {
+            "fieldPath": "initialDelay",
+            "columnName": "initial_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "intervalDuration",
+            "columnName": "interval_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "flexDuration",
+            "columnName": "flex_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "runAttemptCount",
+            "columnName": "run_attempt_count",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "backoffPolicy",
+            "columnName": "backoff_policy",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "backoffDelayDuration",
+            "columnName": "backoff_delay_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "periodStartTime",
+            "columnName": "period_start_time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "minimumRetentionDuration",
+            "columnName": "minimum_retention_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "scheduleRequestedAt",
+            "columnName": "schedule_requested_at",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "expedited",
+            "columnName": "run_in_foreground",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "outOfQuotaPolicy",
+            "columnName": "out_of_quota_policy",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiredNetworkType",
+            "columnName": "required_network_type",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "constraints.mRequiresCharging",
+            "columnName": "requires_charging",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiresDeviceIdle",
+            "columnName": "requires_device_idle",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiresBatteryNotLow",
+            "columnName": "requires_battery_not_low",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiresStorageNotLow",
+            "columnName": "requires_storage_not_low",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mTriggerContentUpdateDelay",
+            "columnName": "trigger_content_update_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mTriggerMaxContentDelay",
+            "columnName": "trigger_max_content_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mContentUriTriggers",
+            "columnName": "content_uri_triggers",
+            "affinity": "BLOB",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_WorkSpec_schedule_requested_at",
+            "unique": false,
+            "columnNames": [
+              "schedule_requested_at"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkSpec_schedule_requested_at` ON `${TABLE_NAME}` (`schedule_requested_at`)"
+          },
+          {
+            "name": "index_WorkSpec_period_start_time",
+            "unique": false,
+            "columnNames": [
+              "period_start_time"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkSpec_period_start_time` ON `${TABLE_NAME}` (`period_start_time`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "WorkTag",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `work_spec_id` TEXT NOT NULL, PRIMARY KEY(`tag`, `work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "tag",
+            "columnName": "tag",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "tag",
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_WorkTag_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkTag_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "SystemIdInfo",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `system_id` INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "systemId",
+            "columnName": "system_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkName",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `work_spec_id` TEXT NOT NULL, PRIMARY KEY(`name`, `work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "name",
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_WorkName_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkName_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkProgress",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `progress` BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "mWorkSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mProgress",
+            "columnName": "progress",
+            "affinity": "BLOB",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "Preference",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `long_value` INTEGER, PRIMARY KEY(`key`))",
+        "fields": [
+          {
+            "fieldPath": "mKey",
+            "columnName": "key",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mValue",
+            "columnName": "long_value",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "key"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c103703e120ae8cc73c9248622f3cd1e')"
+    ]
+  }
+}
\ No newline at end of file